Initial commmit.

reactors
Thomas Hintz 3 years ago
commit 15257b917a

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

34
.gitignore vendored

@ -0,0 +1,34 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
*~

@ -0,0 +1,38 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.jsx`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

@ -0,0 +1 @@
{"version":"1.1.0","chapters":[{"startTime":2.0,"title":"Introduction to this episode."},{"startTime":181.0,"title":"Goal: build a weather app using React server components."},{"startTime":498.0,"title":"Data and data fetching is one of the biggest places where things could change. "},{"startTime":673.0,"title":"Server Side Rendering on the Server."},{"startTime":941.0,"title":"Fetch load and process data in a much more natural and efficient way with responsive clients."},{"startTime":1060.0,"title":"Adding client side interactivity."},{"startTime":1216.0,"title":"[Ad] Dive deep into the motivations and mechanics behind some very successful people.","url":"https://www.forwarddrinkingpodcast.com","img":"https://storage.buzzsprout.com/variants/jzp1bmnjrzryk78pb7iqev70j9dg/6861a7550229613e3387373f20ad829ba4bc5767dd8eb92e70a0abe304d4e657"},{"startTime":1259.86,"title":"(Cont.) Adding client side interactivity."},{"startTime":1391.86,"title":"Passing data from the client to the server. "},{"startTime":1537.86,"title":"Whats new in the react server component world. "},{"startTime":1770.86,"title":"React server components will fundamentally change how were going to fetch data and react. "},{"startTime":1907.86,"title":"server components can access server-side data sources directly, such as databases, file systems, and micro-services. "}]}

1269
feed.rss

File diff suppressed because it is too large Load Diff

@ -0,0 +1,8 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

@ -0,0 +1,9 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
experimental: {
appDir: true,
},
}
module.exports = nextConfig

9714
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,40 @@
{
"name": "thereactshow",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"browserslist": "defaults, not ie <= 11",
"dependencies": {
"@extractus/feed-extractor": "^6.2.1",
"@headlessui/react": "^1.7.7",
"@heroicons/react": "^2.0.13",
"@next/font": "13.1.4",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/line-clamp": "^0.4.2",
"@tailwindcss/typography": "^0.5.7",
"clsx": "^1.2.1",
"eslint": "8.32.0",
"eslint-config-next": "13.1.4",
"focus-visible": "^5.2.0",
"i": "^0.3.7",
"next": "13.1.4",
"nodemailer": "^6.9.1",
"postcss-focus-visible": "^6.0.4",
"react": "18.2.0",
"react-aria": "^3.19.0",
"react-dom": "18.2.0",
"react-stately": "^3.17.0",
"sanitize-html": "^2.8.1",
"srtparsejs": "^1.0.8"
},
"devDependencies": {
"autoprefixer": "^10.4.13",
"postcss": "^8.4.21",
"tailwindcss": "^3.2.4"
}
}

@ -0,0 +1,9 @@
module.exports = {
plugins: {
tailwindcss: {},
'postcss-focus-visible': {
replaceWith: '[data-focus-visible-added]',
},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="31" fill="none"><g opacity=".9"><path fill="url(#a)" d="M13 .4v29.3H7V6.3h-.2L0 10.5V5L7.2.4H13Z"/><path fill="url(#b)" d="M28.8 30.1c-2.2 0-4-.3-5.7-1-1.7-.8-3-1.8-4-3.1a7.7 7.7 0 0 1-1.4-4.6h6.2c0 .8.3 1.4.7 2 .4.5 1 .9 1.7 1.2.7.3 1.6.4 2.5.4 1 0 1.7-.2 2.5-.5.7-.3 1.3-.8 1.7-1.4.4-.6.6-1.2.6-2s-.2-1.5-.7-2.1c-.4-.6-1-1-1.8-1.4-.8-.4-1.8-.5-2.9-.5h-2.7v-4.6h2.7a6 6 0 0 0 2.5-.5 4 4 0 0 0 1.7-1.3c.4-.6.6-1.3.6-2a3.5 3.5 0 0 0-2-3.3 5.6 5.6 0 0 0-4.5 0 4 4 0 0 0-1.7 1.2c-.4.6-.6 1.2-.6 2h-6c0-1.7.6-3.2 1.5-4.5 1-1.3 2.2-2.3 3.8-3C25 .4 26.8 0 28.8 0s3.8.4 5.3 1.1c1.5.7 2.7 1.7 3.6 3a7.2 7.2 0 0 1 1.2 4.2c0 1.6-.5 3-1.5 4a7 7 0 0 1-4 2.2v.2c2.2.3 3.8 1 5 2.2a6.4 6.4 0 0 1 1.6 4.6c0 1.7-.5 3.1-1.4 4.4a9.7 9.7 0 0 1-4 3.1c-1.7.8-3.7 1.1-5.8 1.1Z"/></g><defs><linearGradient id="a" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient><linearGradient id="b" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

@ -0,0 +1,10 @@
import StandardHead from '@/components/StandardHead';
export default async function Head({ params }) {
return (
<StandardHead
title="Download Foundations of High-Performance React"
description="Download Foundations of High-Performance React"
/>
);
};

@ -0,0 +1,30 @@
import JustifiedSection from '@/components/JustifiedSection'
export default async function Page({ params }) {
return (
<>
<JustifiedSection title="Thanks for your support!">
<div className="mt-10 flex items-center justify-center gap-x-6">
<a
href="#"
className="rounded-md bg-white px-3.5 py-1.5 text-base font-semibold leading-7 text-gray-900 shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
>
Download PDF
</a>
<a
href="#"
className="rounded-md bg-white px-3.5 py-1.5 text-base font-semibold leading-7 text-gray-900 shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
>
Download ePub
</a>
<a
href="#"
className="rounded-md bg-white px-3.5 py-1.5 text-base font-semibold leading-7 text-gray-900 shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
>
Download Kindle
</a>
</div>
</JustifiedSection>
</>
)
}

@ -0,0 +1,10 @@
import StandardHead from '@/components/StandardHead';
export default async function Head({ params }) {
return (
<StandardHead
title="Foundations of High-Performance React"
description="A book diving deep into the details of how React actually works."
/>
);
};

@ -0,0 +1,191 @@
import Head from 'next/head'
import Image from 'next/image'
import BookPurchaseCTA from '@/components/BookPurchaseCTA'
import BookTitle from '@/components/BookTitle'
import JustifiedSection from '@/components/JustifiedSection'
import JustifiedSectionReversed from '@/components/JustifiedSectionReversed'
import { Box } from '@/components/Components'
import bookImage from '@/images/foundations.png'
import authorImage from '@/images/headshot.jpg'
const chapters = [
'Preface',
'Acknowledgments',
'Introduction',
'Components of React',
'Markup in JavaScript: JSX',
'Getting Ready to Render with createElement',
'Render: Putting Elements on the Screen',
'Reconciliation, or How React Diffs',
'Fibers: Splitting up Render',
'Putting it all together',
'Conclusion'
];
export default async function Page({ params }) {
return (
<>
<Head>
<title>Foundations of High-Performance React</title>
<meta name="description" content="TODO" />
</Head>
<div className="relative isolate overflow-hidden bg-white">
<svg
className="absolute inset-0 -z-10 h-full w-full stroke-gray-200 [mask-image:radial-gradient(100%_100%_at_top_right,white,transparent)]"
aria-hidden="true"
>
<defs>
<pattern
id="0787a7c5-978c-4f66-83c7-11c213f99cb7"
width={200}
height={200}
x="50%"
y={-1}
patternUnits="userSpaceOnUse"
>
<path d="M.5 200V.5H200" fill="none" />
</pattern>
</defs>
<rect width="100%" height="100%" strokeWidth={0} fill="url(#0787a7c5-978c-4f66-83c7-11c213f99cb7)" />
</svg>
<div className="mx-auto max-w-7xl px-6 pt-10 pb-24 sm:pb-32 lg:flex lg:py-40 lg:px-8">
<div className="mx-auto max-w-2xl lg:mx-0 lg:max-w-xl lg:flex-shrink-0 lg:pt-8">
<h1 className="mt-10 text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">
It All Starts With Understanding The Foundation
</h1>
<p className="mt-6 text-lg leading-8 text-gray-600">
It can be hard to create high-performance React applications without having a firm understanding of the foundations. In this book you&apos;ll create your own version of React that will give you a deep insight into the performance of React itself.
</p>
<div className="mt-10 flex items-center gap-x-6">
<a
href="#first-cta"
className="rounded-md bg-indigo-600 px-3.5 py-1.5 text-base font-semibold leading-7 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
Download
</a>
<a href="#learn-more" className="text-base font-semibold leading-7 text-gray-900">
Learn more <span aria-hidden="true"></span>
</a>
</div>
</div>
<div className="mx-auto mt-16 flex max-w-2xl sm:mt-24 lg:ml-10 lg:mt-0 lg:mr-0 lg:max-w-none lg:flex-none xl:ml-32">
<div className="max-w-3xl flex-none sm:max-w-5xl lg:max-w-none">
<div className="-m-2 rounded-xl bg-gray-900/5 p-2 ring-1 ring-inset ring-gray-900/10 lg:-m-4 lg:rounded-2xl lg:p-4">
<Image
src={bookImage}
alt="App screenshot"
width={2432}
height={1442}
className="w-[33rem] rounded-md shadow-2xl ring-1 ring-gray-900/10"
/>
</div>
</div>
</div>
</div>
</div>
<BookPurchaseCTA />
<JustifiedSection title="About the Book">
<p id="learn-more">
React performance can be a mystery. When your app performance degrades it isn&apos;t always clear where to look or how to fix the issue. Foundations of High-Performance React Applications is a mini-book exploring what makes React itself behave the way it does. Armed with this knowledge you will be better equipped to build your own high-performance React applications and correctly diagnose bottlenecks.
</p>
<p>
Beyond diagnosing bottlenecks, this book teaches the fundamentals of how React renders. By the end of the book you will not only know exactly how React works internally but youll also have a deep understanding of how to build React applications that take full advantage of the strengths of the React architecture.
</p>
<p>
The book takes you through the process of creating your own mini version of React that is based on the same heuristic algorithms React is. You will not only learn how React renders but be able to see it demonstrated in the included source code.
</p>
</JustifiedSection>
<JustifiedSectionReversed title="Table of Contents" bg="gray-800">
<ol className="text-xl">
{chapters.map((c, i) => (
<li key={i}>
{c}
</li>
))}
</ol>
</JustifiedSectionReversed>
<section className="overflow-hidden bg-gray-50 py-12 md:py-20 lg:py-24">
<div className="relative mx-auto max-w-7xl px-6 lg:px-8">
<svg
className="absolute top-full right-full translate-x-1/3 -translate-y-1/4 transform lg:translate-x-1/2 xl:-translate-y-1/2"
width={404}
height={404}
fill="none"
viewBox="0 0 404 404"
role="img"
aria-labelledby="svg-workcation"
>
<title id="svg-workcation">Workcation</title>
<defs>
<pattern
id="ad119f34-7694-4c31-947f-5c9d249b21f3"
x={0}
y={0}
width={20}
height={20}
patternUnits="userSpaceOnUse"
>
<rect x={0} y={0} width={4} height={4} className="text-gray-200" fill="currentColor" />
</pattern>
</defs>
<rect width={404} height={404} fill="url(#ad119f34-7694-4c31-947f-5c9d249b21f3)" />
</svg>
<div className="relative">
<blockquote className="mt-10">
<div className="mx-auto max-w-3xl text-center text-2xl font-medium leading-9 text-gray-900">
<p>
&ldquo;I might be new to React but it was entertaining and clearly communicated.&rdquo;
</p>
</div>
<footer className="mt-8">
<div className="md:flex md:items-center md:justify-center">
<div className="md:flex-shrink-0">
</div>
<div className="mt-3 text-center md:mt-0 md:ml-4 md:flex md:items-center">
<div className="text-base font-medium text-gray-900">Andrew H.</div>
</div>
</div>
</footer>
</blockquote>
</div>
</div>
</section>
<JustifiedSectionReversed title="Book Sample" bg="gray-800">
<div className="mt-10 flex items-center justify-center gap-x-6">
<a
href="/files/foundations-high-performance-react-sample-export.pdf"
target="_blank"
rel="noopener noreferrer"
className="rounded-md bg-white px-3.5 py-1.5 text-base font-semibold leading-7 text-gray-900 shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
>
Download PDF
</a>
</div>
</JustifiedSectionReversed>
<BookPurchaseCTA />
<JustifiedSectionReversed title={(
<Box flex flex-col items-center>
<BookTitle>About the Author</BookTitle>
<Image
src={authorImage}
alt="Picture of smiling author"
width={230}
height={230}
className="rounded-full mt-16"
/>
</Box>
)} bg="gray-800">
<p className="">
Thomas Hintz has been a web developer for two decades and has been in the software industry for 13 years as an engineer and engineering manager. Creator of the 3L Operating System, a Lisp Compiler, web server, web sockets library, and much more.
</p>
<p>
Author of wildly popular essays like Work When You Feel Like It, creator of the fastest websockets server library, and serial speaker at bay-area software events.
</p>
</JustifiedSectionReversed>
</>
)
}

@ -0,0 +1,10 @@
import NavBar from '@/components/NavBar'
export default function ExtraLayout({children}) {
return (
<div>
<NavBar showPodcast />
{children}
</div>
);
}

@ -0,0 +1,18 @@
import { Container } from '@/components/Container'
export default async function Page({}) {
return (
<div className="pt-16 pb-12 sm:pb-4 lg:pt-12">
<Container>
<h1 className="text-2xl font-bold leading-7 text-slate-900">
Contact Us
</h1>
<div className="divide-y divide-slate-100 sm:mt-4 lg:mt-8 lg:border-t lg:border-slate-100">
<p>
Message sent successfully! Thank you!
</p>
</div>
</Container>
</div>
);
}

@ -0,0 +1,10 @@
import StandardHead from '@/components/StandardHead';
export default async function Head({ params }) {
return (
<StandardHead
title="Contact Us - The React Show"
description="Details on how to contact The React Show."
/>
);
};

@ -0,0 +1,155 @@
import { redirect } from 'next/navigation';
import nodemailer from 'nodemailer';
import sanitizeHtml from 'sanitize-html';
import { Container } from '@/components/Container'
export default async function Page({ searchParams }) {
const firstName = searchParams['first-name'];
const lastName = searchParams['last-name'];
const email = searchParams['email'];
const message = searchParams['message'];
const submitted = firstName || lastName || email || message;
const valid = submitted && firstName && lastName && email && message;
let emailSentSuccessfully = false;
if (valid) {
const transporter = nodemailer.createTransport({
host: process.env.CONTACT_HOST,
port: 587,
secure: false, // true for 465, false for other ports
auth: {
user: process.env.CONTACT_USER,
pass: process.env.CONTACT_PASSWORD,
},
});
// send mail with defined transport object
await transporter.sendMail({
from: `"${firstName} ${lastName}" <${process.env.CONTACT_FROM_ADDRESS}>`,
replyTo: `"${firstName} ${lastName}" <${email}>`,
to: process.env.CONTACT_TO_ADDRESS,
subject: "The React Show - Form Submission",
text: message,
html: sanitizeHtml(message, {
allowedTags: [],
allowedAttributes: {}
}),
});
redirect('/contact-success')
}
return (
<div className="pt-16 pb-12 sm:pb-4 lg:pt-12">
<Container>
<h1 className="text-2xl font-bold leading-7 text-slate-900">
Contact Us
</h1>
{valid && !emailSentSuccessfully && (
<div className="divide-y divide-slate-100 sm:mt-4 lg:mt-8 lg:border-t lg:border-slate-100">
<p>
Unable to send message. Please go back and reload the page and try again or try again later. Sorry!
</p>
</div>
)}
{!valid && (
<div className="divide-y divide-slate-100 sm:mt-4 lg:mt-8 lg:border-t lg:border-slate-100">
<p className="mt-4 italic">
Like the show? Want to hear us talk about something specific? Or just want to say hi? Wed love to hear from you!
</p>
<form className="space-y-8">
<div className="space-y-8">
<div className="pt-8">
<div className="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
<div className="sm:col-span-3">
<label htmlFor="first-name" className="block text-sm font-medium text-gray-700">
First name
</label>
<div className="mt-1">
<input
type="text"
name="first-name"
id="first-name"
autoComplete="given-name"
defaultValue={firstName}
required
title="First/Given Name (required)"
className="block w-full rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border-gray-300"
/>
</div>
</div>
<div className="sm:col-span-3">
<label htmlFor="last-name" className="block text-sm font-medium text-gray-700">
Last name
</label>
<div className="mt-1">
<input
type="text"
name="last-name"
id="last-name"
autoComplete="family-name"
defaultValue={lastName}
required
title="Last/Family Name (required)"
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
/>
</div>
</div>
<div className="sm:col-span-4">
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email address
</label>
<div className="mt-1">
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
defaultValue={email}
title="Email Address (required)"
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
/>
</div>
</div>
</div>
</div>
<div className="sm:col-span-6">
<label htmlFor="message" className="block text-sm font-medium text-gray-700">
Your Message
</label>
<div className="mt-1">
<textarea
id="message"
name="message"
rows={3}
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
defaultValue={message}
required
title="Your message to us! (required)"
/>
</div>
</div>
</div>
<div className="pt-5">
<div className="flex justify-end">
<button
type="submit"
className="ml-3 inline-flex justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
Send
</button>
</div>
</div>
</form>
</div>
)}
</Container>
</div>
);
}

@ -0,0 +1,10 @@
import StandardHead from '@/components/StandardHead';
export default function Head({ params }) {
return (
<StandardHead
title="The React Show - Weekly React Focused Podcast"
description="Weekly podcast focused on React, programming, and software engineering."
/>
);
}

@ -0,0 +1,280 @@
import { Fragment, useId } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import clsx from 'clsx'
import NavBar from '@/components/NavBar'
import posterImage from '@/images/poster.png'
function randomBetween(min, max, seed = 1) {
return () => {
let rand = Math.sin(seed++) * 10000
rand = rand - Math.floor(rand)
return Math.floor(rand * (max - min + 1) + min)
}
}
function Waveform(props) {
let id = useId()
let bars = {
total: 100,
width: 2,
gap: 2,
minHeight: 40,
maxHeight: 100,
}
let barHeights = Array.from(
{ length: bars.total },
randomBetween(bars.minHeight, bars.maxHeight)
)
return (
<svg aria-hidden="true" {...props}>
<defs>
<linearGradient id={`${id}-fade`} x1="0" x2="0" y1="0" y2="1">
<stop offset="40%" stopColor="white" />
<stop offset="100%" stopColor="black" />
</linearGradient>
<linearGradient id={`${id}-gradient`}>
<stop offset="0%" stopColor="#4989E8" />
<stop offset="50%" stopColor="#6159DA" />
<stop offset="100%" stopColor="#FF54AD" />
</linearGradient>
<mask id={`${id}-mask`}>
<rect width="100%" height="100%" fill={`url(#${id}-pattern)`} />
</mask>
<pattern
id={`${id}-pattern`}
width={bars.total * bars.width + bars.total * bars.gap}
height="100%"
patternUnits="userSpaceOnUse"
>
{Array.from({ length: bars.total }, (_, index) => (
<rect
key={index}
width={bars.width}
height={`${barHeights[index]}%`}
x={bars.gap * (index + 1) + bars.width * index}
fill={`url(#${id}-fade)`}
/>
))}
</pattern>
</defs>
<rect
width="100%"
height="100%"
fill={`url(#${id}-gradient)`}
mask={`url(#${id}-mask)`}
opacity="0.25"
/>
</svg>
)
}
function TinyWaveFormIcon({ colors = [], ...props }) {
return (
<svg aria-hidden="true" viewBox="0 0 10 10" {...props}>
<path
d="M0 5a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V5Z"
className={colors[0]}
/>
<path
d="M6 1a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1Z"
className={colors[1]}
/>
</svg>
)
}
function SpotifyIcon(props) {
return (
<svg aria-hidden="true" viewBox="0 0 32 32" {...props}>
<path d="M15.8 3a12.8 12.8 0 1 0 0 25.6 12.8 12.8 0 0 0 0-25.6Zm5.87 18.461a.8.8 0 0 1-1.097.266c-3.006-1.837-6.787-2.252-11.244-1.234a.796.796 0 1 1-.355-1.555c4.875-1.115 9.058-.635 12.432 1.427a.8.8 0 0 1 .265 1.096Zm1.565-3.485a.999.999 0 0 1-1.371.33c-3.44-2.116-8.685-2.728-12.755-1.493a1 1 0 0 1-.58-1.91c4.65-1.41 10.428-.726 14.378 1.7a1 1 0 0 1 .33 1.375l-.002-.002Zm.137-3.629c-4.127-2.45-10.933-2.675-14.871-1.478a1.196 1.196 0 1 1-.695-2.291c4.52-1.374 12.037-1.107 16.785 1.711a1.197 1.197 0 1 1-1.221 2.06" />
</svg>
)
}
function ApplePodcastIcon(props) {
return (
<svg aria-hidden="true" viewBox="0 0 32 32" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M27.528 24.8c-.232.592-.768 1.424-1.536 2.016-.44.336-.968.664-1.688.88-.768.232-1.72.304-2.904.304H10.6c-1.184 0-2.128-.08-2.904-.304a4.99 4.99 0 0 1-1.688-.88c-.76-.584-1.304-1.424-1.536-2.016C4.008 23.608 4 22.256 4 21.4V10.6c0-.856.008-2.208.472-3.4.232-.592.768-1.424 1.536-2.016.44-.336.968-.664 1.688-.88C8.472 4.08 9.416 4 10.6 4h10.8c1.184 0 2.128.08 2.904.304a4.99 4.99 0 0 1 1.688.88c.76.584 1.304 1.424 1.536 2.016C28 8.392 28 9.752 28 10.6v10.8c0 .856-.008 2.208-.472 3.4Zm-9.471-6.312a1.069 1.069 0 0 0-.32-.688c-.36-.376-.992-.624-1.736-.624-.745 0-1.377.24-1.737.624-.183.2-.287.4-.32.688-.063.558-.024 1.036.04 1.807v.009c.065.736.184 1.72.336 2.712.112.712.2 1.096.28 1.368.136.448.625.832 1.4.832.776 0 1.273-.392 1.4-.832.08-.272.169-.656.28-1.368.152-1 .273-1.976.337-2.712.072-.776.104-1.256.04-1.816ZM16 16.375c1.088 0 1.968-.88 1.968-1.967 0-1.08-.88-1.968-1.968-1.968s-1.968.88-1.968 1.968.88 1.967 1.968 1.967Zm-.024-9.719c-4.592.016-8.352 3.744-8.416 8.336-.048 3.72 2.328 6.904 5.648 8.072.08.032.16-.04.152-.12a35.046 35.046 0 0 0-.041-.288c-.029-.192-.057-.384-.079-.576a.317.317 0 0 0-.168-.232 7.365 7.365 0 0 1-4.424-6.824c.04-4 3.304-7.256 7.296-7.288 4.088-.032 7.424 3.28 7.424 7.36 0 3.016-1.824 5.608-4.424 6.752a.272.272 0 0 0-.168.232l-.12.864c-.016.088.072.152.152.12a8.448 8.448 0 0 0 5.648-7.968c-.016-4.656-3.816-8.448-8.48-8.44Zm-5.624 8.376c.04-2.992 2.44-5.464 5.432-5.576 3.216-.128 5.88 2.456 5.872 5.64a5.661 5.661 0 0 1-2.472 4.672c-.08.056-.184-.008-.176-.096.016-.344.024-.648.008-.96 0-.104.04-.2.112-.272a4.584 4.584 0 0 0 1.448-3.336 4.574 4.574 0 0 0-4.752-4.568 4.585 4.585 0 0 0-4.392 4.448 4.574 4.574 0 0 0 1.448 3.456c.08.072.12.168.112.272-.016.32-.016.624.008.968 0 .088-.104.144-.176.096a5.65 5.65 0 0 1-2.472-4.744Z"
/>
</svg>
)
}
function OvercastIcon(props) {
return (
<svg aria-hidden="true" viewBox="0 0 32 32" {...props}>
<path d="M16 28.8A12.77 12.77 0 0 1 3.2 16 12.77 12.77 0 0 1 16 3.2 12.77 12.77 0 0 1 28.8 16 12.77 12.77 0 0 1 16 28.8Zm0-5.067.96-.96-.96-3.68-.96 3.68.96.96Zm-1.226-.054-.48 1.814 1.12-1.12-.64-.694Zm2.453 0-.64.64 1.12 1.12-.48-1.76Zm.907 3.307L16 24.853l-2.133 2.133c.693.107 1.387.213 2.133.213.747 0 1.44-.053 2.134-.213ZM16 4.799C9.814 4.8 4.8 9.813 4.8 16c0 4.907 3.147 9.067 7.52 10.56l2.4-8.906c-.533-.374-.853-1.014-.853-1.707A2.14 2.14 0 0 1 16 13.813a2.14 2.14 0 0 1 2.134 2.133c0 .693-.32 1.28-.854 1.707l2.4 8.906A11.145 11.145 0 0 0 27.2 16c0-6.186-5.013-11.2-11.2-11.2Zm7.307 16.747c-.267.32-.747.427-1.12.16-.373-.267-.427-.747-.16-1.067 0 0 1.44-1.92 1.44-4.64 0-2.72-1.44-4.64-1.44-4.64-.267-.32-.213-.8.16-1.066.373-.267.853-.16 1.12.16.107.106 1.76 2.293 1.76 5.546 0 3.254-1.653 5.44-1.76 5.547Zm-3.893-2.08c-.32-.32-.267-.907.053-1.227 0 0 .8-.853.8-2.24 0-1.386-.8-2.186-.8-2.24-.32-.32-.32-.853-.053-1.226.32-.374.8-.374 1.12-.054.053.054 1.333 1.387 1.333 3.52 0 2.134-1.28 3.467-1.333 3.52-.32.32-.8.267-1.12-.053Zm-6.827 0c-.32.32-.8.373-1.12.053-.053-.106-1.333-1.386-1.333-3.52 0-2.133 1.28-3.413 1.333-3.52.32-.32.853-.32 1.12.054.32.32.267.906-.053 1.226 0 .054-.8.854-.8 2.24 0 1.387.8 2.24.8 2.24.32.32.373.854.053 1.227Zm-2.773 2.24c-.374.267-.854.16-1.12-.16-.107-.107-1.76-2.293-1.76-5.547 0-3.253 1.653-5.44 1.76-5.546.266-.32.746-.427 1.12-.16.373.266.426.746.16 1.066 0 0-1.44 1.92-1.44 4.64 0 2.72 1.44 4.64 1.44 4.64.266.32.16.8-.16 1.067Z" />
</svg>
)
}
function RSSIcon(props) {
return (
<svg aria-hidden="true" viewBox="0 0 32 32" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.5 4h15A4.5 4.5 0 0 1 28 8.5v15a4.5 4.5 0 0 1-4.5 4.5h-15A4.5 4.5 0 0 1 4 23.5v-15A4.5 4.5 0 0 1 8.5 4ZM13 22a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm-6-6a9 9 0 0 1 9 9h3A12 12 0 0 0 7 13v3Zm5.74-4.858A15 15 0 0 0 7 10V7a18 18 0 0 1 18 18h-3a15 15 0 0 0-9.26-13.858Z"
/>
</svg>
)
}
function PersonIcon(props) {
return (
<svg aria-hidden="true" viewBox="0 0 11 12" {...props}>
<path d="M5.019 5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Zm3.29 7c1.175 0 2.12-1.046 1.567-2.083A5.5 5.5 0 0 0 5.019 7 5.5 5.5 0 0 0 .162 9.917C-.39 10.954.554 12 1.73 12h6.578Z" />
</svg>
)
}
function AboutSection(props) {
/* let [isExpanded, setIsExpanded] = useState(false) */
const isExpanded = false
const setIsExpanded = {}
/* onClick={() => setIsExpanded(true)} */
return (
<section {...props}>
<h2 className="flex items-center font-mono text-sm font-medium leading-7 text-slate-900">
<TinyWaveFormIcon
colors={['fill-violet-300', 'fill-pink-300']}
className="h-2.5 w-2.5"
/>
<span className="ml-2.5">About</span>
</h2>
<p
className="mt-2 text-base leading-7 text-slate-700"
>
In this show, Thomas digs deep to understand React and how best to utilize it while discussing real world experiences with: React, programming, and software engineering. Tune in every Friday (usually) to hear the latest in the React community.
</p>
</section>
)
}
function Layout({ children }) {
let hosts = ['Thomas Hintz']
return (
<>
<header className="bg-slate-50 lg:fixed lg:inset-y-0 lg:left-0 lg:flex lg:w-112 lg:items-start lg:overflow-y-auto xl:w-120">
<div className="hidden lg:sticky lg:top-0 lg:flex lg:w-16 lg:flex-none lg:items-center lg:whitespace-nowrap lg:py-12 lg:text-sm lg:leading-7 lg:[writing-mode:vertical-rl]">
<span className="font-mono text-slate-500">Hosted by</span>
<span className="mt-6 flex gap-6 font-bold text-slate-900">
{hosts.map((host, hostIndex) => (
<Fragment key={host}>
{hostIndex !== 0 && (
<span aria-hidden="true" className="text-slate-400">
/
</span>
)}
{host}
</Fragment>
))}
</span>
</div>
<div className="relative z-10 mx-auto pb-4 pt-10 md:max-w-2xl lg:min-h-full lg:flex-auto lg:border-x lg:border-slate-200 lg:py-12">
<div className="px-4 sm:px-6 md:px-4 lg:px-8 xl:px-12 mb-10">
<Link
href="/"
className="relative mx-auto block w-48 overflow-hidden rounded-lg bg-slate-200 shadow-xl shadow-slate-200 sm:w-64 sm:rounded-xl lg:w-auto lg:rounded-2xl"
aria-label="Homepage"
>
<Image
className="w-full"
src={posterImage}
alt=""
sizes="(min-width: 1024px) 20rem, (min-width: 640px) 16rem, 12rem"
priority
/>
<div className="absolute inset-0 rounded-lg ring-1 ring-inset ring-black/10 sm:rounded-xl lg:rounded-2xl" />
</Link>
</div>
<NavBar />
<div className="mt-6 text-center lg:mt-12 lg:text-left px-4 sm:px-6 md:px-4 lg:px-8 xl:px-12">
<p className="text-xl font-bold text-slate-900">
<Link href="/">The React Show</Link>
</p>
<p className="mt-3 text-lg font-medium leading-8 text-slate-700">
A podcast focused on React, programming in general, and the intersection of programming and the rest of the world. Come join us on this journey.
</p>
</div>
<div className="px-4 sm:px-6 md:px-4 lg:px-8 xl:px-12">
<AboutSection className="mt-12 hidden lg:block" />
<section className="mt-10 lg:mt-12">
<h2 className="sr-only flex items-center font-mono text-sm font-medium leading-7 text-slate-900 lg:not-sr-only">
<TinyWaveFormIcon
colors={['fill-indigo-300', 'fill-blue-300']}
className="h-2.5 w-2.5"
/>
<span className="ml-2.5">Listen</span>
</h2>
<div className="h-px bg-gradient-to-r from-slate-200/0 via-slate-200 to-slate-200/0 lg:hidden" />
<ul
role="list"
className="mt-4 flex justify-center gap-10 text-base font-medium leading-7 text-slate-700 sm:gap-8 lg:flex-col lg:gap-4"
>
{[
['Spotify', SpotifyIcon, 'https://open.spotify.com/show/7tqGIm6Y1g5idqU87TcpJv'],
['Apple Podcast', ApplePodcastIcon, 'https://podcasts.apple.com/podcast/id1566939019'],
['Overcast', OvercastIcon, 'https://overcast.fm/itunes1566939019'],
['RSS Feed', RSSIcon, 'https://feeds.buzzsprout.com/1764837.rss'],
].map(([label, Icon, url]) => (
<li key={label} className="flex">
<Link
href={url}
className="group flex items-center"
aria-label={label}
>
<Icon className="h-8 w-8 fill-slate-400 group-hover:fill-slate-600" />
<span className="hidden sm:ml-3 sm:block">{label}</span>
</Link>
</li>
))}
</ul>
</section>
</div>
</div>
</header>
<main className="border-t border-slate-200 lg:relative lg:mb-28 lg:ml-112 lg:border-t-0 xl:ml-120">
<Waveform className="absolute left-0 top-0 h-20 w-full" />
<div className="relative">{children}</div>
</main>
<footer className="border-t border-slate-200 bg-slate-50 py-10 pb-40 sm:py-16 sm:pb-32 lg:hidden">
<div className="mx-auto px-4 sm:px-6 md:max-w-2xl md:px-4">
<AboutSection />
<h2 className="mt-8 flex items-center font-mono text-sm font-medium leading-7 text-slate-900">
<PersonIcon className="h-3 w-auto fill-slate-300" />
<span className="ml-2.5">Hosted by</span>
</h2>
<div className="mt-2 flex gap-6 text-sm font-bold leading-7 text-slate-900">
{hosts.map((host, hostIndex) => (
<Fragment key={host}>
{hostIndex !== 0 && (
<span aria-hidden="true" className="text-slate-400">
/
</span>
)}
{host}
</Fragment>
))}
</div>
</div>
</footer>
</>
)
}
export default function MainLayout({children}) {
return (
<Layout>
{children}
</Layout>
);
}

@ -0,0 +1,8 @@
export default function Loading() {
// You can add any UI inside Loading, including a Skeleton.
return (
<>
<h1>Loading...</h1>
</>
)
}

@ -0,0 +1,245 @@
import { Suspense } from "react";
import Link from 'next/link'
import { ArrowLongLeftIcon, ArrowLongRightIcon } from '@heroicons/react/20/solid'
import { Container } from '@/components/Container'
import { FormattedDate } from '@/components/FormattedDate'
import { getEpisodes } from '@/data/episodes'
import Player from '@/app/Player'
function EpisodeEntry({ episode }) {
let date = new Date(episode.published)
return (
<article
aria-labelledby={`episode-${episode.id}-title`}
className="py-10 sm:py-12"
>
<Container>
<div className="flex flex-col items-start">
<h2
id={`episode-${episode.id}-title`}
className="mt-2 text-lg font-bold text-slate-900"
>
<Link href={`/podcast/${episode?.slug}`}>{episode.title}</Link>
</h2>
<FormattedDate
date={date}
className="order-first font-mono text-sm leading-7 text-slate-500"
/>
<p className="mt-1 text-base leading-7 text-slate-700">
{episode.description}
</p>
<div className="mt-4 flex items-center gap-4">
<Player episode={episode}>
<span className="ml-3" aria-hidden="true">
Listen
</span>
</Player>
<span
aria-hidden="true"
className="text-sm font-bold text-slate-400"
>
/
</span>
<Link
href={`/podcast/${episode.slug}`}
className="flex items-center text-sm font-bold leading-6 text-pink-500 hover:text-pink-700 active:text-pink-900"
aria-label={`Show notes for episode ${episode.title}`}
>
Show notes
</Link>
{episode?.transcript &&
(
<>
<span
aria-hidden="true"
className="text-sm font-bold text-slate-400"
>
/
</span>
<Link
href={`/podcast/${episode.slug}#transcript`}
className="flex items-center text-sm font-bold leading-6 text-pink-500 hover:text-pink-700 active:text-pink-900"
aria-label={`Transcript for episode ${episode.title}`}
>
Transcript
</Link>
</>
)}
</div>
</div>
</Container>
</article>
)
}
function pageClasses(page, i) {
return `inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 ${page === i ? 'border-indigo-500 text-indigo-600': ''}`
};
async function Content({ page }) {
const episodes = await getEpisodes()
await new Promise(resolve => setTimeout(resolve, 1000));
const pages = Math.ceil(episodes.length / 10);
return (
<>
<div className="divide-y divide-slate-100 sm:mt-4 lg:mt-8 lg:border-t lg:border-slate-100">
{episodes.slice((page - 1) * 10, page * 10).map((episode) => (
<EpisodeEntry key={episode.id} episode={episode} />
))}
</div>
<Container>
<nav className="flex items-center justify-between border-t border-gray-200 px-4 sm:px-0">
<div className="-mt-px flex w-0 flex-1">
<Link
href={`/?page=${page - 1}`}
className={`inline-flex items-center border-t-2 border-transparent pt-4 pr-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 ${page <= 1 ? 'pointer-events-none' : ''}`}
>
<ArrowLongLeftIcon className="mr-3 h-5 w-5 text-gray-400" aria-hidden="true" />
Previous
</Link>
</div>
<div className="hidden md:-mt-px md:flex">
{page > 2 && (
<Link
href="/?page=1"
className={pageClasses(page, 1)}
>
1
</Link>
)}
{page > 2 && (
<span className="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500">
...
</span>
)}
{page > 1 && (
<Link
href={`/?page=${page - 1}`}
className={pageClasses(page - 1, page)}
>
{page - 1}
</Link>
)}
<Link
href={`/?page=${page}`}
className={pageClasses(page, page)}
>
{page}
</Link>
{page < pages && (
<Link
href={`/?page=${page + 1}`}
className={pageClasses(page + 1, page)}
>
{page + 1}
</Link>
)}
{(page + 1) < pages && (
<span className="inline-flex items-center border-t-2 border-transparent px-4 pt-4 text-sm font-medium text-gray-500">
...
</span>
)}
{(page + 1) < pages && (
<Link
href={`/?page=${pages}`}
className={pageClasses(page, pages)}
>
{pages}
</Link>
)}
</div>
<div className="-mt-px flex w-0 flex-1 justify-end">
<Link
href={`/?page=${page + 1}`}
className={`inline-flex items-center border-t-2 border-transparent pt-4 pl-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 ${page >= pages ? 'pointer-events-none' : ''}`}
>
Next
<ArrowLongRightIcon className="ml-3 h-5 w-5 text-gray-400" aria-hidden="true" />
</Link>
</div>
</nav>
</Container>
</>
);
}
function Skeleton({ width, height, className, color }) {
const w = width ? '' : 'w-full';
const c = color ? color : 'bg-slate-200';
return (
<div className={`animate-pulse flex ${c} ${w} ${className}`} style={{ width, height }} />
);
};
function EpisodeEntryLoading() {
return (
<div
className="py-10 sm:py-12"
>
<Container>
<div className="flex flex-col items-start w-full">
<div className="order-first font-mono text-sm leading-7 text-slate-500">
<Skeleton height="1.75rem" width="140px" />
</div>
<h2
className="mt-2 text-lg font-bold text-slate-900 w-full"
>
<Skeleton height="1.75rem" />
</h2>
<div className="mt-1 text-base leading-7 text-slate-700 w-full">
<Skeleton height="1rem" className="mt-3" />
<Skeleton height="1rem" className="mt-3" />
<Skeleton height="1rem" className="mt-3" />
</div>
<div className="mt-4 flex items-center gap-4">
<Skeleton width="0.625rem" height="0.625rem" />
<span className="ml-3">
<Skeleton height="1.5rem" width="42.275px" />
</span>
<span
aria-hidden="true"
className="text-sm font-bold text-slate-400"
>
/
</span>
<Skeleton height="1.5rem" width="80.175px" />
</div>
</div>
</Container>
</div>
)
}
function Loading() {
// You can add any UI inside Loading, including a Skeleton.
return (
<div className="divide-y divide-slate-100 sm:mt-4 lg:mt-8 lg:border-t lg:border-slate-100">
{[0, 1, 2, 3, 4, 5, 6, 7].map((i) => (
<EpisodeEntryLoading key={i} />
))}
</div>
);
}
export default async function Home({ searchParams }) {
return (
<>
<div className="pt-16 pb-12 sm:pb-4 lg:pt-12">
<Container>
<h1 className="text-2xl font-bold leading-7 text-slate-900">
Episodes
</h1>
</Container>
<Suspense fallback={<Loading />}>
<Content page={searchParams?.page ? parseInt(searchParams?.page, 10) : 1} />
</Suspense>
</div>
</>
)
}

@ -0,0 +1 @@
{"version":"1.1.0","chapters":[{"startTime":2.0,"title":"Introduction to this episode."},{"startTime":181.0,"title":"Goal: build a weather app using React server components."},{"startTime":498.0,"title":"Data and data fetching is one of the biggest places where things could change. "},{"startTime":673.0,"title":"Server Side Rendering on the Server."},{"startTime":941.0,"title":"Fetch load and process data in a much more natural and efficient way with responsive clients."},{"startTime":1060.0,"title":"Adding client side interactivity."},{"startTime":1216.0,"title":"[Ad] Dive deep into the motivations and mechanics behind some very successful people.","url":"https://www.forwarddrinkingpodcast.com","img":"https://storage.buzzsprout.com/variants/jzp1bmnjrzryk78pb7iqev70j9dg/6861a7550229613e3387373f20ad829ba4bc5767dd8eb92e70a0abe304d4e657"},{"startTime":1259.86,"title":"(Cont.) Adding client side interactivity."},{"startTime":1391.86,"title":"Passing data from the client to the server. "},{"startTime":1537.86,"title":"Whats new in the react server component world. "},{"startTime":1770.86,"title":"React server components will fundamentally change how were going to fetch data and react. "},{"startTime":1907.86,"title":"server components can access server-side data sources directly, such as databases, file systems, and micro-services. "}]}

@ -0,0 +1,13 @@
import { getEpisode } from '@/data/episodes'
import StandardHead from '@/components/StandardHead';
export default async function Head({ params }) {
const episode = await getEpisode({ episodeSlug: params.slug })
return (
<StandardHead
title={`${episode.title} - The React Show`}
description={episode.description}
/>
);
};

@ -0,0 +1,104 @@
/* import chaptersStatic from './chapters.json' assert { type: 'json' }; */
import Head from 'next/head'
import { Container } from '@/components/Container'
import { FormattedDate } from '@/components/FormattedDate'
import { PlayButtonClient } from '@/components/player/PlayButtonClient'
import Player from '@/app/Player'
import { getEpisode } from '@/data/episodes'
function parseTime(time) {
const stepOne = time.split(',');
const hms = stepOne.length > 0 && stepOne[0].split(':')
const h = parseInt(hms[0], 10);
const m = parseInt(hms[1], 10);
const s = parseInt(hms[2], 10);
return (h * 3600) + (m * 60) + s;
}
function humanTime(time) {
const stepOne = time.split(',');
return stepOne.length > 0 ? stepOne[0] : '';
}
export default async function Page({ params }) {
const episode = await getEpisode({ episodeSlug: params.slug })
const chaptersRes = episode?.chapters && await fetch(episode.chapters, { cache: 'no-store' });
/* const { chapters } = chaptersStatic */
const { chapters } = chaptersRes ? await chaptersRes.json() : { chapters: null }
let chapterOffsets = [[0, 0]]
if (chapters) {
chapters.reduce(({ startTime: prevStartTime, title: prevTitle, acc }, { title, startTime }) => {
const containsAd = prevTitle.includes('[Ad]')
if (containsAd) {
chapterOffsets.push([prevStartTime, acc + (startTime - prevStartTime)])
}
return { startTime, title, acc: containsAd ? acc + (startTime - prevStartTime) : acc }
}, { startTime: 0, title: '', acc: 0 })
}
chapterOffsets = chapterOffsets.reverse()
let date = new Date(episode.published)
let audioPlayerData = {
title: episode.title,
audio: {
src: episode.audio?.src,
type: episode.audio?.type,
},
link: `/${episode.slug}`,
}
return (
<>
<Head>
<title>{`${episode.title} - Their Side`}</title>
<meta name="description" content={episode.description} />
</Head>
<article className="py-16 lg:py-36">
<Container>
<header className="flex flex-col">
<div className="flex items-center gap-6">
<PlayButtonClient audioPlayerData={audioPlayerData} size="large" />
<div className="flex flex-col">
<h1 className="mt-2 text-4xl font-bold text-slate-900">
{episode.title}
</h1>
<FormattedDate
date={date}
className="order-first font-mono text-sm leading-7 text-slate-500"
/>
</div>
</div>
<p className="ml-24 mt-3 text-lg font-medium leading-8 text-slate-700">
{episode.description}
</p>
</header>
<hr className="my-12 border-gray-200" />
<div
className="prose prose-slate mt-14 [&>h2]:mt-12 [&>h2]:flex [&>h2]:items-center [&>h2]:font-mono [&>h2]:text-sm [&>h2]:font-medium [&>h2]:leading-7 [&>h2]:text-slate-900 [&>h2]:before:mr-3 [&>h2]:before:h-3 [&>h2]:before:w-1.5 [&>h2]:before:rounded-r-full [&>h2]:before:bg-cyan-200 [&>ul]:mt-6 [&>ul]:list-['\2013\20'] [&>ul]:pl-5 [&>h2:nth-of-type(3n+2)]:before:bg-indigo-200 [&>h2:nth-of-type(3n)]:before:bg-violet-200"
dangerouslySetInnerHTML={{ __html: episode.content || 'CONTENT' }}
/>
{episode?.transcript && (
<>
<hr className="my-12 border-gray-200" />
<h2 className="mt-2 text-3xl font-bold text-slate-900" id="transcript">Transcript</h2>
<div className="space-y-4">
{episode.transcript.map(({ id, startTime, endTime, text }) => (
<p key={id}>
<Player
episode={episode}
startTime={parseTime(startTime) + chapterOffsets.find(([start]) => parseTime(startTime) > start)[1]}
endTime={parseTime(endTime) + chapterOffsets.find(([start]) => parseTime(startTime) > start)[1]} />
<strong><time>{humanTime(startTime)}</time></strong> {text}
</p>
))}
</div>
</>
)}
</Container>
</article>
</>
)
}

@ -0,0 +1,52 @@
'use client'
import { useMemo } from 'react'
import { useAudioPlayer } from '@/components/AudioProvider'
function PlayPauseIcon({ playing, ...props }) {
return (
<svg aria-hidden="true" viewBox="0 0 10 10" fill="none" {...props}>
{playing ? (
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1.496 0a.5.5 0 0 0-.5.5v9a.5.5 0 0 0 .5.5H2.68a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5H1.496Zm5.82 0a.5.5 0 0 0-.5.5v9a.5.5 0 0 0 .5.5H8.5a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5H7.316Z"
/>
) : (
<path d="M8.25 4.567a.5.5 0 0 1 0 .866l-7.5 4.33A.5.5 0 0 1 0 9.33V.67A.5.5 0 0 1 .75.237l7.5 4.33Z" />
)}
</svg>
)
}
export default function Player({ episode, startTime, endTime, children }) {
let audioPlayerData = useMemo(
() => ({
title: episode.title,
audio: {
src: episode.audio.src,
type: episode.audio.type,
},
link: `/${episode.id}`,
}),
[episode]
)
let player = useAudioPlayer(audioPlayerData)
const withinTimeframe = player.currentTime >= startTime && player.currentTime <= endTime
return (
<button
onClick={() => { startTime ? (withinTimeframe ? player.toggle() : player.play(startTime)) : player.toggle(); }}
type="button"
className={`${startTime ? 'inline-flex mr-2': 'flex'} items-center text-sm font-bold leading-6 text-pink-500 hover:text-pink-700 active:text-pink-900`}
aria-label={`${player.playing ? 'Pause' : 'Play'} episode ${
episode.title
}`}
>
<PlayPauseIcon
playing={startTime ? player.playing && withinTimeframe : player.playing}
className="h-2.5 w-2.5 fill-current"
/>
{children}
</button>
);
}

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@ -0,0 +1,10 @@
export default function Head() {
return (
<>
<title>Create Next App</title>
<meta content="width=device-width, initial-scale=1" name="viewport" />
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</>
)
}

@ -0,0 +1,19 @@
import './globals.css';
import { AudioPlayer } from '@/components/player/AudioPlayer'
import { AudioProvider } from '@/components/AudioProvider'
export default function RootLayout({children}) {
return (
<html lang="en">
<body>
<AudioProvider>
{children}
<div className="fixed inset-x-0 bottom-0 z-10 lg:left-112 xl:left-120">
<AudioPlayer />
</div>
</AudioProvider>
</body>
</html>
);
}

@ -0,0 +1,271 @@
.main {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 6rem;
min-height: 100vh;
}
.description {
display: inherit;
justify-content: inherit;
align-items: inherit;
font-size: 0.85rem;
max-width: var(--max-width);
width: 100%;
z-index: 2;
font-family: var(--font-mono);
}
.description a {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.description p {
position: relative;
margin: 0;
padding: 1rem;
background-color: rgba(var(--callout-rgb), 0.5);
border: 1px solid rgba(var(--callout-border-rgb), 0.3);
border-radius: var(--border-radius);
}
.code {
font-weight: 700;
font-family: var(--font-mono);
}
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(33%, auto));
width: var(--max-width);
max-width: 100%;
}
.card {
padding: 1rem 1.2rem;
border-radius: var(--border-radius);
background: rgba(var(--card-rgb), 0);
border: 1px solid rgba(var(--card-border-rgb), 0);
transition: background 200ms, border 200ms;
}
.card span {
display: inline-block;
transition: transform 200ms;
}
.card h2 {
font-weight: 600;
margin-bottom: 0.7rem;
}
.card p {
margin: 0;
opacity: 0.6;
font-size: 0.9rem;
line-height: 1.5;
max-width: 34ch;
}
.center {
display: flex;
justify-content: center;
align-items: center;
position: relative;
padding: 4rem 0;
}
.center::before {
background: var(--secondary-glow);
border-radius: 50%;
width: 480px;
height: 360px;
margin-left: -400px;
}
.center::after {
background: var(--primary-glow);
width: 240px;
height: 180px;
z-index: -1;
}
.center::before,
.center::after {
content: '';
left: 50%;
position: absolute;
filter: blur(45px);
transform: translateZ(0);
}
.logo,
.thirteen {
position: relative;
}
.thirteen {
display: flex;
justify-content: center;
align-items: center;
width: 75px;
height: 75px;
padding: 25px 10px;
margin-left: 16px;
transform: translateZ(0);
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: 0px 2px 8px -1px #0000001a;
}
.thirteen::before,
.thirteen::after {
content: '';
position: absolute;
z-index: -1;
}
/* Conic Gradient Animation */
.thirteen::before {
animation: 6s rotate linear infinite;
width: 200%;
height: 200%;
background: var(--tile-border);
}
/* Inner Square */
.thirteen::after {
inset: 0;
padding: 1px;
border-radius: var(--border-radius);
background: linear-gradient(
to bottom right,
rgba(var(--tile-start-rgb), 1),
rgba(var(--tile-end-rgb), 1)
);
background-clip: content-box;
}
/* Enable hover only on non-touch devices */
@media (hover: hover) and (pointer: fine) {
.card:hover {
background: rgba(var(--card-rgb), 0.1);
border: 1px solid rgba(var(--card-border-rgb), 0.15);
}
.card:hover span {
transform: translateX(4px);
}
}
@media (prefers-reduced-motion) {
.thirteen::before {
animation: none;
}
.card:hover span {
transform: none;
}
}
/* Mobile and Tablet */
@media (max-width: 1023px) {
.content {
padding: 4rem;
}
.grid {
grid-template-columns: 1fr;
margin-bottom: 120px;
max-width: 320px;
text-align: center;
}
.card {
padding: 1rem 2.5rem;
}
.card h2 {
margin-bottom: 0.5rem;
}
.center {
padding: 8rem 0 6rem;
}
.center::before {
transform: none;
height: 300px;
}
.description {
font-size: 0.8rem;
}
.description a {
padding: 1rem;
}
.description p,
.description div {
display: flex;
justify-content: center;
position: fixed;
width: 100%;
}
.description p {
align-items: center;
inset: 0 0 auto;
padding: 2rem 1rem 1.4rem;
border-radius: 0;
border: none;
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
background: linear-gradient(
to bottom,
rgba(var(--background-start-rgb), 1),
rgba(var(--callout-rgb), 0.5)
);
background-clip: padding-box;
backdrop-filter: blur(24px);
}
.description div {
align-items: flex-end;
pointer-events: none;
inset: auto 0 0;
padding: 2rem;
height: 200px;
background: linear-gradient(
to bottom,
transparent 0%,
rgb(var(--background-end-rgb)) 40%
);
z-index: 1;
}
}
@media (prefers-color-scheme: dark) {
.vercelLogo {
filter: invert(1);
}
.logo,
.thirteen img {
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
}
}
@keyframes rotate {
from {
transform: rotate(360deg);
}
to {
transform: rotate(0deg);
}
}

@ -0,0 +1,139 @@
'use client'
import { createContext, useContext, useMemo, useReducer, useRef } from 'react'
const AudioPlayerContext = createContext()
const reducers = {
SET_META(state, action) {
return { ...state, meta: action.payload }
},
PLAY(state, _action) {
return { ...state, playing: true }
},
PAUSE(state, _action) {
return { ...state, playing: false }
},
TOGGLE_MUTE(state, _action) {
return { ...state, muted: !state.muted }
},
SET_CURRENT_TIME(state, action) {
return { ...state, currentTime: action.payload }
},
SET_DURATION(state, action) {
return { ...state, duration: action.payload }
},
}
function audioReducer(state, action) {
return reducers[action.type](state, action)
}
export function AudioProvider({ children }) {
let [state, dispatch] = useReducer(audioReducer, {
playing: false,
muted: false,
duration: 0,
currentTime: 0,
meta: null,
})
let playerRef = useRef(null)
let actions = useMemo(() => {
return {
play(data, startTime) {
if (data) {
dispatch({ type: 'SET_META', payload: data })
if (playerRef.current.currentSrc !== data.audio.src) {
let playbackRate = playerRef.current.playbackRate
playerRef.current.src = data.audio.src
playerRef.current.load()
playerRef.current.pause()
playerRef.current.playbackRate = playbackRate
playerRef.currentTime = 0
}
}
if (startTime) {
playerRef.current.currentTime = startTime
}
playerRef.current.play()
},
pause() {
playerRef.current.pause()
},
toggle(data) {
this.isPlaying(data) ? actions.pause() : actions.play(data)
},
seekBy(amount) {
playerRef.current.currentTime += amount
},
seek(time) {
playerRef.current.currentTime = time
},
playbackRate(rate) {
playerRef.current.playbackRate = rate
},
toggleMute() {
dispatch({ type: 'TOGGLE_MUTE' })
},
isPlaying(data) {
return data
? state.playing && playerRef.current.currentSrc === data.audio.src
: state.playing
},
}
}, [state.playing])
let api = useMemo(() => ({ ...state, ...actions }), [state, actions])
return (
<>
<AudioPlayerContext.Provider value={api}>
{children}
</AudioPlayerContext.Provider>
<audio
ref={playerRef}
onPlay={() => dispatch({ type: 'PLAY' })}
onPause={() => dispatch({ type: 'PAUSE' })}
onTimeUpdate={(event) => {
dispatch({
type: 'SET_CURRENT_TIME',
payload: Math.floor(event.target.currentTime),
})
}}
onDurationChange={(event) => {
dispatch({
type: 'SET_DURATION',
payload: Math.floor(event.target.duration),
})
}}
muted={state.muted}
/>
</>
)
}
export function useAudioPlayer(data) {
let player = useContext(AudioPlayerContext)
return useMemo(
() => ({
...player,
play(startTime) {
player.play(data, startTime)
},
toggle() {
player.toggle(data)
},
get playing() {
return player.isPlaying(data)
},
get currentTime() {
return player.currentTime
}
}),
[player, data]
)
}

@ -0,0 +1,19 @@
import CenteredDarkPanel from '@/components/CenteredDarkPanel'
export default function BookPurchaseCTA() {
return (
<>
<CenteredDarkPanel title="Foundations of High-Performance React" buttonText="Purchase"
id="first-cta"
href="https://buy.stripe.com/bIY6rpgbG3d2ces000"
>
<p className="mx-auto mt-6 max-w-xl text-lg leading-8 text-gray-300">
$11.99
</p>
<p className="mx-auto mt-6 max-w-xl text-lg leading-8 text-gray-300">
DRM-free Kindle, ePub, & PDF downloads.
</p>
</CenteredDarkPanel>
</>
);
}

@ -0,0 +1,7 @@
export default function BookTitle({ children }) {
return (
<h2 className="text-4xl font-bold tracking-tight text-white-900">
{children}
</h2>
);
}

@ -0,0 +1,43 @@
export default function CenteredDarkPanel({ title, children, buttonText, id, href }) {
return (
<div className="bg-white" id={id}>
<div className="mx-auto max-w-7xl py-6 sm:px-6 sm:py-8 lg:px-8">
<div className="relative isolate overflow-hidden bg-orange-700 px-6 py-24 text-center shadow-2xl sm:rounded-3xl sm:px-16">
<h2 className="mx-auto max-w-2xl text-4xl font-bold tracking-tight text-white">
{title}
</h2>
{children}
<div className="mt-10 flex items-center justify-center gap-x-6">
<a
href={href}
className="rounded-md bg-white px-3.5 py-1.5 text-base font-semibold leading-7 text-gray-900 shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
>
{buttonText}
</a>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1024 1024"
className="absolute top-1/2 left-1/2 -z-10 h-[64rem] w-[64rem] -translate-x-1/2"
aria-hidden="true"
>
<circle cx={512} cy={512} r={512} fill="url(#827591b1-ce8c-4110-b064-7cb85a0b1217)" fillOpacity="0.7" />
<defs>
<radialGradient
id="827591b1-ce8c-4110-b064-7cb85a0b1217"
cx={0}
cy={0}
r={1}
gradientUnits="userSpaceOnUse"
gradientTransform="translate(512 512) rotate(90) scale(512)"
>
<stop stopColor="#7775D6" />
<stop offset={1} stopColor="#E935C1" stopOpacity={0} />
</radialGradient>
</defs>
</svg>
</div>
</div>
</div>
)
}

@ -0,0 +1,8 @@
export function Box({ children, ...props }) {
const classNames = Object.entries(props).reduce((acc, [k]) => `${acc} ${k}`, '')
return (
<div className={classNames}>
{children}
</div>
);
};

@ -0,0 +1,13 @@
import clsx from 'clsx'
export function Container({ className, children, ...props }) {
return (
<div className={clsx('lg:px-8', className)} {...props}>
<div className="lg:max-w-4xl">
<div className="mx-auto px-4 sm:px-6 md:max-w-2xl md:px-4 lg:px-0">
{children}
</div>
</div>
</div>
)
}

@ -0,0 +1,16 @@
const dateFormatter = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
export function FormattedDate({ date, ...props }) {
if (date instanceof Date && !isNaN(date.valueOf())) {
return (
<time dateTime={date.toISOString()} {...props}>
{dateFormatter.format(date)}
</time>
)
}
return null;
}

@ -0,0 +1,8 @@
export default function HeadTags() {
return (
<>
<meta content="width=device-width, initial-scale=1" name="viewport" />
<link rel="icon" href="/favicon.ico" />
</>
)
};

@ -0,0 +1,14 @@
export default function JustifiedSection({ title, children, bg = 'white' }) {
return (
<div className={`bg-${bg}`}>
<div className="mx-auto max-w-7xl px-6 py-24 sm:py-32 lg:flex lg:items-center lg:justify-between lg:px-8">
<h2 className="text-4xl font-bold tracking-tight text-gray-900">
{title}
</h2>
<div className="mt-10 flex items-center gap-x-6 lg:mt-0 lg:max-w-prose flex-wrap space-y-4">
{children}
</div>
</div>
</div>
)
}

@ -0,0 +1,15 @@
import BookTitle from '@/components/BookTitle'
export default function JustifiedSectionReversed({ title, children, bg = 'white' }) {
return (
<div className={`bg-${bg}`}>
<div className="mx-auto max-w-7xl px-6 py-24 sm:py-32 lg:flex lg:items-center lg:justify-between lg:px-8 text-white">
{title && typeof title === 'string' && <BookTitle>{title}</BookTitle>}
{title && typeof title !== 'string' && title}
<div className="mt-10 flex items-center gap-x-6 lg:mt-0 lg:max-w-prose flex-wrap space-y-4">
{children}
</div>
</div>
</div>
)
}

@ -0,0 +1,285 @@
import { Fragment, useId, useState } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import clsx from 'clsx'
import { AudioPlayer } from '@/components/player/AudioPlayer'
import posterImage from '@/images/poster.png'
function randomBetween(min, max, seed = 1) {
return () => {
let rand = Math.sin(seed++) * 10000
rand = rand - Math.floor(rand)
return Math.floor(rand * (max - min + 1) + min)
}
}
function Waveform(props) {
let id = useId()
let bars = {
total: 100,
width: 2,
gap: 2,
minHeight: 40,
maxHeight: 100,
}
let barHeights = Array.from(
{ length: bars.total },
randomBetween(bars.minHeight, bars.maxHeight)
)
return (
<svg aria-hidden="true" {...props}>
<defs>
<linearGradient id={`${id}-fade`} x1="0" x2="0" y1="0" y2="1">
<stop offset="40%" stopColor="white" />
<stop offset="100%" stopColor="black" />
</linearGradient>
<linearGradient id={`${id}-gradient`}>
<stop offset="0%" stopColor="#4989E8" />
<stop offset="50%" stopColor="#6159DA" />
<stop offset="100%" stopColor="#FF54AD" />
</linearGradient>
<mask id={`${id}-mask`}>
<rect width="100%" height="100%" fill={`url(#${id}-pattern)`} />
</mask>
<pattern
id={`${id}-pattern`}
width={bars.total * bars.width + bars.total * bars.gap}
height="100%"
patternUnits="userSpaceOnUse"
>
{Array.from({ length: bars.total }, (_, index) => (
<rect
key={index}
width={bars.width}
height={`${barHeights[index]}%`}
x={bars.gap * (index + 1) + bars.width * index}
fill={`url(#${id}-fade)`}
/>
))}
</pattern>
</defs>
<rect
width="100%"
height="100%"
fill={`url(#${id}-gradient)`}
mask={`url(#${id}-mask)`}
opacity="0.25"
/>
</svg>
)
}
function TinyWaveFormIcon({ colors = [], ...props }) {
return (
<svg aria-hidden="true" viewBox="0 0 10 10" {...props}>
<path
d="M0 5a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V5Z"
className={colors[0]}
/>
<path
d="M6 1a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1Z"
className={colors[1]}
/>
</svg>
)
}
function SpotifyIcon(props) {
return (
<svg aria-hidden="true" viewBox="0 0 32 32" {...props}>
<path d="M15.8 3a12.8 12.8 0 1 0 0 25.6 12.8 12.8 0 0 0 0-25.6Zm5.87 18.461a.8.8 0 0 1-1.097.266c-3.006-1.837-6.787-2.252-11.244-1.234a.796.796 0 1 1-.355-1.555c4.875-1.115 9.058-.635 12.432 1.427a.8.8 0 0 1 .265 1.096Zm1.565-3.485a.999.999 0 0 1-1.371.33c-3.44-2.116-8.685-2.728-12.755-1.493a1 1 0 0 1-.58-1.91c4.65-1.41 10.428-.726 14.378 1.7a1 1 0 0 1 .33 1.375l-.002-.002Zm.137-3.629c-4.127-2.45-10.933-2.675-14.871-1.478a1.196 1.196 0 1 1-.695-2.291c4.52-1.374 12.037-1.107 16.785 1.711a1.197 1.197 0 1 1-1.221 2.06" />
</svg>
)
}
function ApplePodcastIcon(props) {
return (
<svg aria-hidden="true" viewBox="0 0 32 32" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M27.528 24.8c-.232.592-.768 1.424-1.536 2.016-.44.336-.968.664-1.688.88-.768.232-1.72.304-2.904.304H10.6c-1.184 0-2.128-.08-2.904-.304a4.99 4.99 0 0 1-1.688-.88c-.76-.584-1.304-1.424-1.536-2.016C4.008 23.608 4 22.256 4 21.4V10.6c0-.856.008-2.208.472-3.4.232-.592.768-1.424 1.536-2.016.44-.336.968-.664 1.688-.88C8.472 4.08 9.416 4 10.6 4h10.8c1.184 0 2.128.08 2.904.304a4.99 4.99 0 0 1 1.688.88c.76.584 1.304 1.424 1.536 2.016C28 8.392 28 9.752 28 10.6v10.8c0 .856-.008 2.208-.472 3.4Zm-9.471-6.312a1.069 1.069 0 0 0-.32-.688c-.36-.376-.992-.624-1.736-.624-.745 0-1.377.24-1.737.624-.183.2-.287.4-.32.688-.063.558-.024 1.036.04 1.807v.009c.065.736.184 1.72.336 2.712.112.712.2 1.096.28 1.368.136.448.625.832 1.4.832.776 0 1.273-.392 1.4-.832.08-.272.169-.656.28-1.368.152-1 .273-1.976.337-2.712.072-.776.104-1.256.04-1.816ZM16 16.375c1.088 0 1.968-.88 1.968-1.967 0-1.08-.88-1.968-1.968-1.968s-1.968.88-1.968 1.968.88 1.967 1.968 1.967Zm-.024-9.719c-4.592.016-8.352 3.744-8.416 8.336-.048 3.72 2.328 6.904 5.648 8.072.08.032.16-.04.152-.12a35.046 35.046 0 0 0-.041-.288c-.029-.192-.057-.384-.079-.576a.317.317 0 0 0-.168-.232 7.365 7.365 0 0 1-4.424-6.824c.04-4 3.304-7.256 7.296-7.288 4.088-.032 7.424 3.28 7.424 7.36 0 3.016-1.824 5.608-4.424 6.752a.272.272 0 0 0-.168.232l-.12.864c-.016.088.072.152.152.12a8.448 8.448 0 0 0 5.648-7.968c-.016-4.656-3.816-8.448-8.48-8.44Zm-5.624 8.376c.04-2.992 2.44-5.464 5.432-5.576 3.216-.128 5.88 2.456 5.872 5.64a5.661 5.661 0 0 1-2.472 4.672c-.08.056-.184-.008-.176-.096.016-.344.024-.648.008-.96 0-.104.04-.2.112-.272a4.584 4.584 0 0 0 1.448-3.336 4.574 4.574 0 0 0-4.752-4.568 4.585 4.585 0 0 0-4.392 4.448 4.574 4.574 0 0 0 1.448 3.456c.08.072.12.168.112.272-.016.32-.016.624.008.968 0 .088-.104.144-.176.096a5.65 5.65 0 0 1-2.472-4.744Z"
/>
</svg>
)
}
function OvercastIcon(props) {
return (
<svg aria-hidden="true" viewBox="0 0 32 32" {...props}>
<path d="M16 28.8A12.77 12.77 0 0 1 3.2 16 12.77 12.77 0 0 1 16 3.2 12.77 12.77 0 0 1 28.8 16 12.77 12.77 0 0 1 16 28.8Zm0-5.067.96-.96-.96-3.68-.96 3.68.96.96Zm-1.226-.054-.48 1.814 1.12-1.12-.64-.694Zm2.453 0-.64.64 1.12 1.12-.48-1.76Zm.907 3.307L16 24.853l-2.133 2.133c.693.107 1.387.213 2.133.213.747 0 1.44-.053 2.134-.213ZM16 4.799C9.814 4.8 4.8 9.813 4.8 16c0 4.907 3.147 9.067 7.52 10.56l2.4-8.906c-.533-.374-.853-1.014-.853-1.707A2.14 2.14 0 0 1 16 13.813a2.14 2.14 0 0 1 2.134 2.133c0 .693-.32 1.28-.854 1.707l2.4 8.906A11.145 11.145 0 0 0 27.2 16c0-6.186-5.013-11.2-11.2-11.2Zm7.307 16.747c-.267.32-.747.427-1.12.16-.373-.267-.427-.747-.16-1.067 0 0 1.44-1.92 1.44-4.64 0-2.72-1.44-4.64-1.44-4.64-.267-.32-.213-.8.16-1.066.373-.267.853-.16 1.12.16.107.106 1.76 2.293 1.76 5.546 0 3.254-1.653 5.44-1.76 5.547Zm-3.893-2.08c-.32-.32-.267-.907.053-1.227 0 0 .8-.853.8-2.24 0-1.386-.8-2.186-.8-2.24-.32-.32-.32-.853-.053-1.226.32-.374.8-.374 1.12-.054.053.054 1.333 1.387 1.333 3.52 0 2.134-1.28 3.467-1.333 3.52-.32.32-.8.267-1.12-.053Zm-6.827 0c-.32.32-.8.373-1.12.053-.053-.106-1.333-1.386-1.333-3.52 0-2.133 1.28-3.413 1.333-3.52.32-.32.853-.32 1.12.054.32.32.267.906-.053 1.226 0 .054-.8.854-.8 2.24 0 1.387.8 2.24.8 2.24.32.32.373.854.053 1.227Zm-2.773 2.24c-.374.267-.854.16-1.12-.16-.107-.107-1.76-2.293-1.76-5.547 0-3.253 1.653-5.44 1.76-5.546.266-.32.746-.427 1.12-.16.373.266.426.746.16 1.066 0 0-1.44 1.92-1.44 4.64 0 2.72 1.44 4.64 1.44 4.64.266.32.16.8-.16 1.067Z" />
</svg>
)
}
function RSSIcon(props) {
return (
<svg aria-hidden="true" viewBox="0 0 32 32" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.5 4h15A4.5 4.5 0 0 1 28 8.5v15a4.5 4.5 0 0 1-4.5 4.5h-15A4.5 4.5 0 0 1 4 23.5v-15A4.5 4.5 0 0 1 8.5 4ZM13 22a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm-6-6a9 9 0 0 1 9 9h3A12 12 0 0 0 7 13v3Zm5.74-4.858A15 15 0 0 0 7 10V7a18 18 0 0 1 18 18h-3a15 15 0 0 0-9.26-13.858Z"
/>
</svg>
)
}
function PersonIcon(props) {
return (
<svg aria-hidden="true" viewBox="0 0 11 12" {...props}>
<path d="M5.019 5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Zm3.29 7c1.175 0 2.12-1.046 1.567-2.083A5.5 5.5 0 0 0 5.019 7 5.5 5.5 0 0 0 .162 9.917C-.39 10.954.554 12 1.73 12h6.578Z" />
</svg>
)
}
function AboutSection(props) {
let [isExpanded, setIsExpanded] = useState(false)
return (
<section {...props}>
<h2 className="flex items-center font-mono text-sm font-medium leading-7 text-slate-900">
<TinyWaveFormIcon
colors={['fill-violet-300', 'fill-pink-300']}
className="h-2.5 w-2.5"
/>
<span className="ml-2.5">About</span>
</h2>
<p
className={clsx(
'mt-2 text-base leading-7 text-slate-700',
!isExpanded && 'lg:line-clamp-4'
)}
>
In this show, Eric and Wes dig deep to get to the facts with guests who
have been labeled villains by a society quick to judge, without actually
getting the full story. Tune in every Thursday to get to the truth with
another misunderstood outcast as they share the missing context in their
tragic tale.
</p>
{!isExpanded && (
<button
type="button"
className="mt-2 hidden text-sm font-bold leading-6 text-pink-500 hover:text-pink-700 active:text-pink-900 lg:inline-block"
onClick={() => setIsExpanded(true)}
>
Show more
</button>
)}
</section>
)
}
export function Layout({ children }) {
let hosts = ['Eric Gordon', 'Wes Mantooth']
return (
<>
<header className="bg-slate-50 lg:fixed lg:inset-y-0 lg:left-0 lg:flex lg:w-112 lg:items-start lg:overflow-y-auto xl:w-120">
<div className="hidden lg:sticky lg:top-0 lg:flex lg:w-16 lg:flex-none lg:items-center lg:whitespace-nowrap lg:py-12 lg:text-sm lg:leading-7 lg:[writing-mode:vertical-rl]">
<span className="font-mono text-slate-500">Hosted by</span>
<span className="mt-6 flex gap-6 font-bold text-slate-900">
{hosts.map((host, hostIndex) => (
<Fragment key={host}>
{hostIndex !== 0 && (
<span aria-hidden="true" className="text-slate-400">
/
</span>
)}
{host}
</Fragment>
))}
</span>
</div>
<div className="relative z-10 mx-auto px-4 pb-4 pt-10 sm:px-6 md:max-w-2xl md:px-4 lg:min-h-full lg:flex-auto lg:border-x lg:border-slate-200 lg:py-12 lg:px-8 xl:px-12">
<Link
href="/"
className="relative mx-auto block w-48 overflow-hidden rounded-lg bg-slate-200 shadow-xl shadow-slate-200 sm:w-64 sm:rounded-xl lg:w-auto lg:rounded-2xl"
aria-label="Homepage"
>
<Image
className="w-full"
src={posterImage}
alt=""
sizes="(min-width: 1024px) 20rem, (min-width: 640px) 16rem, 12rem"
priority
/>
<div className="absolute inset-0 rounded-lg ring-1 ring-inset ring-black/10 sm:rounded-xl lg:rounded-2xl" />
</Link>
<div className="mt-10 text-center lg:mt-12 lg:text-left">
<p className="text-xl font-bold text-slate-900">
<Link href="/">Their Side</Link>
</p>
<p className="mt-3 text-lg font-medium leading-8 text-slate-700">
Conversations with the most tragically misunderstood people of our
time.
</p>
</div>
<AboutSection className="mt-12 hidden lg:block" />
<section className="mt-10 lg:mt-12">
<h2 className="sr-only flex items-center font-mono text-sm font-medium leading-7 text-slate-900 lg:not-sr-only">
<TinyWaveFormIcon
colors={['fill-indigo-300', 'fill-blue-300']}
className="h-2.5 w-2.5"
/>
<span className="ml-2.5">Listen</span>
</h2>
<div className="h-px bg-gradient-to-r from-slate-200/0 via-slate-200 to-slate-200/0 lg:hidden" />
<ul
role="list"
className="mt-4 flex justify-center gap-10 text-base font-medium leading-7 text-slate-700 sm:gap-8 lg:flex-col lg:gap-4"
>
{[
['Spotify', SpotifyIcon],
['Apple Podcast', ApplePodcastIcon],
['Overcast', OvercastIcon],
['RSS Feed', RSSIcon],
].map(([label, Icon]) => (
<li key={label} className="flex">
<Link
href="/"
className="group flex items-center"
aria-label={label}
>
<Icon className="h-8 w-8 fill-slate-400 group-hover:fill-slate-600" />
<span className="hidden sm:ml-3 sm:block">{label}</span>
</Link>
</li>
))}
</ul>
</section>
</div>
</header>
<main className="border-t border-slate-200 lg:relative lg:mb-28 lg:ml-112 lg:border-t-0 xl:ml-120">
<Waveform className="absolute left-0 top-0 h-20 w-full" />
<div className="relative">{children}</div>
</main>
<footer className="border-t border-slate-200 bg-slate-50 py-10 pb-40 sm:py-16 sm:pb-32 lg:hidden">
<div className="mx-auto px-4 sm:px-6 md:max-w-2xl md:px-4">
<AboutSection />
<h2 className="mt-8 flex items-center font-mono text-sm font-medium leading-7 text-slate-900">
<PersonIcon className="h-3 w-auto fill-slate-300" />
<span className="ml-2.5">Hosted by</span>
</h2>
<div className="mt-2 flex gap-6 text-sm font-bold leading-7 text-slate-900">
{hosts.map((host, hostIndex) => (
<Fragment key={host}>
{hostIndex !== 0 && (
<span aria-hidden="true" className="text-slate-400">
/
</span>
)}
{host}
</Fragment>
))}
</div>
</div>
</footer>
<div className="fixed inset-x-0 bottom-0 z-10 lg:left-112 xl:left-120">
<AudioPlayer />
</div>
</>
)
}

@ -0,0 +1,30 @@
import Link from 'next/link'
export default function NavBar({ showPodcast }) {
return (
<nav className="bg-gray-800">
<ol className="flex space-x-4 p-4 justify-center">
<li>
<Link
href="/book"
className="rounded-md px-3 py-2 text-lg font-medium text-gray-300 hover:bg-gray-700 hover:text-white"
>Book</Link>
</li>
<li>
<Link
href="/contact-us"
className="rounded-md px-3 py-2 text-lg font-medium text-gray-300 hover:bg-gray-700 hover:text-white"
>Contact</Link>
</li>
{showPodcast && (
<li>
<Link
href="/"
className="rounded-md px-3 py-2 text-lg font-medium text-gray-300 hover:bg-gray-700 hover:text-white"
>Podcast</Link>
</li>
)}
</ol>
</nav>
)
}

@ -0,0 +1,14 @@
import HeadTags from '@/components/HeadTags';
export default function StandardHead({ title, description }) {
return (
<>
<HeadTags />
<title>{title}</title>
<meta
name="description"
content={description}
/>
</>
);
};

@ -0,0 +1,97 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import Link from 'next/link'
import { useAudioPlayer } from '@/components/AudioProvider'
import { ForwardButton } from '@/components/player/ForwardButton'
import { MuteButton } from '@/components/player/MuteButton'
import { PlaybackRateButton } from '@/components/player/PlaybackRateButton'
import { PlayButton } from '@/components/player/PlayButton'
import { RewindButton } from '@/components/player/RewindButton'
import { Slider } from '@/components/player/Slider'
function parseTime(seconds) {
let hours = Math.floor(seconds / 3600)
let minutes = Math.floor((seconds - hours * 3600) / 60)
seconds = seconds - hours * 3600 - minutes * 60
return [hours, minutes, seconds]
}
function formatHumanTime(seconds) {
let [h, m, s] = parseTime(seconds)
return `${h} hour${h === 1 ? '' : 's'}, ${m} minute${
m === 1 ? '' : 's'
}, ${s} second${s === 1 ? '' : 's'}`
}
export function AudioPlayer() {
let player = useAudioPlayer()
let wasPlayingRef = useRef(false)
let [currentTime, setCurrentTime] = useState(player.currentTime)
useEffect(() => {
setCurrentTime(null)
}, [player.currentTime])
if (!player.meta) {
return null
}
return (
<div className="flex items-center gap-6 bg-white/90 py-4 px-4 shadow shadow-slate-200/80 ring-1 ring-slate-900/5 backdrop-blur-sm md:px-6">
<div className="hidden md:block">
<PlayButton player={player} size="medium" />
</div>
<div className="mb-[env(safe-area-inset-bottom)] flex flex-1 flex-col gap-3 overflow-hidden p-1">
<Link
href={player.meta.link}
className="truncate text-center text-sm font-bold leading-6 md:text-left"
title={player.meta.title}
>
{player.meta.title}
</Link>
<div className="flex justify-between gap-6">
<div className="flex items-center md:hidden">
<MuteButton player={player} />
</div>
<div className="flex flex-none items-center gap-4">
<RewindButton player={player} />
<div className="md:hidden">
<PlayButton player={player} size="small" />
</div>
<ForwardButton player={player} />
</div>
<Slider
label="Current time"
maxValue={player.duration}
step={1}
value={[currentTime ?? player.currentTime]}
onChange={([v]) => setCurrentTime(v)}
onChangeEnd={(value) => {
player.seek(value)
if (wasPlayingRef.current) {
player.play()
}
}}
numberFormatter={{ format: formatHumanTime }}
onChangeStart={() => {
wasPlayingRef.current = player.playing
player.pause()
}}
/>
<div className="flex items-center gap-4">
<div className="flex items-center">
<PlaybackRateButton player={player} />
</div>
<div className="hidden items-center md:flex">
<MuteButton player={player} />
</div>
</div>
</div>
</div>
</div>
)
}

@ -0,0 +1,38 @@
function ForwardIcon(props) {
return (
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" {...props}>
<path
d="M16 5L19 8M19 8L16 11M19 8H10.5C7.46243 8 5 10.4624 5 13.5C5 15.4826 5.85204 17.2202 7 18.188"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M13 15V19"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16 18V16C16 15.4477 16.4477 15 17 15H18C18.5523 15 19 15.4477 19 16V18C19 18.5523 18.5523 19 18 19H17C16.4477 19 16 18.5523 16 18Z"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export function ForwardButton({ player, amount = 10 }) {
return (
<button
type="button"
className="group relative rounded-full focus:outline-none"
onClick={() => player.seekBy(amount)}
aria-label={`Fast-forward ${amount} seconds`}
>
<div className="absolute -inset-4 -left-2 md:hidden" />
<ForwardIcon className="h-6 w-6 stroke-slate-500 group-hover:stroke-slate-700" />
</button>
)
}

@ -0,0 +1,46 @@
function MuteIcon({ muted, ...props }) {
return (
<svg
aria-hidden="true"
viewBox="0 0 24 24"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
{muted ? (
<>
<path d="M12 6L8 10H6C5.44772 10 5 10.4477 5 11V13C5 13.5523 5.44772 14 6 14H8L12 18V6Z" />
<path d="M16 10L19 13" fill="none" />
<path d="M19 10L16 13" fill="none" />
</>
) : (
<>
<path d="M12 6L8 10H6C5.44772 10 5 10.4477 5 11V13C5 13.5523 5.44772 14 6 14H8L12 18V6Z" />
<path d="M17 7C17 7 19 9 19 12C19 15 17 17 17 17" fill="none" />
<path
d="M15.5 10.5C15.5 10.5 16 10.9998 16 11.9999C16 13 15.5 13.5 15.5 13.5"
fill="none"
/>
</>
)}
</svg>
)
}
export function MuteButton({ player }) {
return (
<button
type="button"
className="group relative rounded-md hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 md:order-none"
onClick={() => player.toggleMute()}
aria-label={player.muted ? 'Unmute' : 'Mute'}
>
<div className="absolute -inset-4 md:hidden" />
<MuteIcon
muted={player.muted}
className="h-6 w-6 fill-slate-500 stroke-slate-500 group-hover:fill-slate-700 group-hover:stroke-slate-700"
/>
</button>
)
}

@ -0,0 +1,64 @@
import clsx from 'clsx'
function PauseIcon(props) {
return (
<svg aria-hidden="true" viewBox="0 0 22 28" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1.5 0C0.671573 0 0 0.671572 0 1.5V26.5C0 27.3284 0.671573 28 1.5 28H4.5C5.32843 28 6 27.3284 6 26.5V1.5C6 0.671573 5.32843 0 4.5 0H1.5ZM17.5 0C16.6716 0 16 0.671572 16 1.5V26.5C16 27.3284 16.6716 28 17.5 28H20.5C21.3284 28 22 27.3284 22 26.5V1.5C22 0.671573 21.3284 0 20.5 0H17.5Z"
/>
</svg>
)
}
function PlayIcon(props) {
return (
<svg aria-hidden="true" viewBox="0 0 36 36" {...props}>
<path d="M33.75 16.701C34.75 17.2783 34.75 18.7217 33.75 19.299L11.25 32.2894C10.25 32.8668 9 32.1451 9 30.9904L9 5.00962C9 3.85491 10.25 3.13323 11.25 3.71058L33.75 16.701Z" />
</svg>
)
}
export function PlayButton({ player, size = 'large' }) {
return (
<button
type="button"
className={clsx(
'group relative flex flex-shrink-0 items-center justify-center rounded-full bg-slate-700 hover:bg-slate-900 focus:outline-none focus:ring-slate-700',
{
large: 'h-18 w-18 focus:ring focus:ring-offset-4',
medium: 'h-14 w-14 focus:ring-2 focus:ring-offset-2',
small: 'h-10 w-10 focus:ring-2 focus:ring-offset-2',
}[size]
)}
onClick={player.toggle}
aria-label={player.playing ? 'Pause' : 'Play'}
>
<div className="absolute -inset-3 md:hidden" />
{player.playing ? (
<PauseIcon
className={clsx(
'fill-white group-active:fill-white/80',
{
large: 'h-7 w-7',
medium: 'h-5 w-5',
small: 'h-4 w-4',
}[size]
)}
/>
) : (
<PlayIcon
className={clsx(
'fill-white group-active:fill-white/80',
{
large: 'h-9 w-9',
medium: 'h-7 w-7',
small: 'h-5 w-5',
}[size]
)}
/>
)}
</button>
)
}

@ -0,0 +1,12 @@
'use client'
import { useAudioPlayer } from '@/components/AudioProvider'
import { PlayButton } from '@/components/player/PlayButton'
export function PlayButtonClient({ audioPlayerData, size }) {
let player = useAudioPlayer(audioPlayerData)
return (
<PlayButton player={player} size={size} />
);
};

@ -0,0 +1,114 @@
import { useState } from 'react'
const playbackRates = [
{
value: 1,
icon: function PlaybackIcon(props) {
return (
<svg
aria-hidden="true"
viewBox="0 0 16 16"
fill="none"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<path
d="M13 1H3C1.89543 1 1 1.89543 1 3V13C1 14.1046 1.89543 15 3 15H13C14.1046 15 15 14.1046 15 13V3C15 1.89543 14.1046 1 13 1Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="2"
/>
<path d="M3.75 7.25L5.25 5.77539V11.25" />
<path d="M8.75 7.75L11.25 10.25" />
<path d="M11.25 7.75L8.75 10.25" />
</svg>
)
},
},
{
value: 1.5,
icon: function PlaybackIcon(props) {
return (
<svg
aria-hidden="true"
viewBox="0 0 16 16"
fill="none"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<path
d="M13 1H3C1.89543 1 1 1.89543 1 3V13C1 14.1046 1.89543 15 3 15H13C14.1046 15 15 14.1046 15 13V3C15 1.89543 14.1046 1 13 1Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="2"
/>
<path d="M2.75 7.25L4.25 5.77539V11.25" />
<path
d="M7.5 11C7.5 11.2761 7.27614 11.5 7 11.5C6.72386 11.5 6.5 11.2761 6.5 11C6.5 10.7239 6.72386 10.5 7 10.5C7.27614 10.5 7.5 10.7239 7.5 11Z"
strokeWidth="1"
/>
<path d="M12.25 5.75H9.75V8.25H10.75C11.5784 8.25 12.25 8.92157 12.25 9.75V9.75C12.25 10.5784 11.5784 11.25 10.75 11.25H9.75" />
</svg>
)
},
},
{
value: 2,
icon: function PlaybackIcon(props) {
return (
<svg
aria-hidden="true"
viewBox="0 0 16 16"
fill="none"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<path
d="M13 1H3C1.89543 1 1 1.89543 1 3V13C1 14.1046 1.89543 15 3 15H13C14.1046 15 15 14.1046 15 13V3C15 1.89543 14.1046 1 13 1Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="2"
/>
<path d="M9.75 8.75L12.25 11.25" />
<path d="M12.25 8.75L9.75 11.25" />
<path d="M3.75 7.25C3.75 7.25 3.90144 5.75 5.63462 5.75C6.1633 5.75 6.5448 5.95936 6.81973 6.25035C7.67157 7.15197 6.97033 8.47328 6.0238 9.28942L3.75 11.25H7.25" />
</svg>
)
},
},
]
export function PlaybackRateButton({ player }) {
let [playbackRate, setPlaybackRate] = useState(playbackRates[0])
return (
<button
type="button"
className="relative flex h-6 w-6 items-center justify-center rounded-md text-slate-500 hover:bg-slate-100 hover:text-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2"
onClick={() => {
setPlaybackRate((rate) => {
let existingIdx = playbackRates.indexOf(rate)
let idx = (existingIdx + 1) % playbackRates.length
let next = playbackRates[idx]
player.playbackRate(next.value)
return next
})
}}
aria-label="Playback rate"
>
<div className="absolute -inset-4 md:hidden" />
<playbackRate.icon className="h-4 w-4" />
</button>
)
}

@ -0,0 +1,31 @@
function RewindIcon(props) {
return (
<svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<path d="M8 5L5 8M5 8L8 11M5 8H13.5C16.5376 8 19 10.4624 19 13.5C19 15.4826 18.148 17.2202 17 18.188" />
<path d="M5 15V19" />
<path d="M8 18V16C8 15.4477 8.44772 15 9 15H10C10.5523 15 11 15.4477 11 16V18C11 18.5523 10.5523 19 10 19H9C8.44772 19 8 18.5523 8 18Z" />
</svg>
)
}
export function RewindButton({ player, amount = 10 }) {
return (
<button
type="button"
className="group relative rounded-full focus:outline-none"
onClick={() => player.seekBy(-amount)}
aria-label={`Rewind ${amount} seconds`}
>
<div className="absolute -inset-4 -right-2 md:hidden" />
<RewindIcon className="h-6 w-6 stroke-slate-500 group-hover:stroke-slate-700" />
</button>
)
}

@ -0,0 +1,160 @@
import { useRef } from 'react'
import {
mergeProps,
useFocusRing,
useSlider,
useSliderThumb,
VisuallyHidden,
} from 'react-aria'
import { useSliderState } from 'react-stately'
import clsx from 'clsx'
function parseTime(seconds) {
let hours = Math.floor(seconds / 3600)
let minutes = Math.floor((seconds - hours * 3600) / 60)
seconds = seconds - hours * 3600 - minutes * 60
return [hours, minutes, seconds]
}
function formatTime(seconds, totalSeconds = seconds) {
let totalWithoutLeadingZeroes = totalSeconds.slice(
totalSeconds.findIndex((x) => x !== 0)
)
return seconds
.slice(seconds.length - totalWithoutLeadingZeroes.length)
.map((x) => x.toString().padStart(2, '0'))
.join(':')
}
function Thumb(props) {
let { state, trackRef, focusProps, isFocusVisible, index } = props
let inputRef = useRef(null)
let { thumbProps, inputProps } = useSliderThumb(
{ index, trackRef, inputRef },
state
)
return (
<div
className="absolute top-1/2 -translate-x-1/2"
style={{
left: `${state.getThumbPercent(index) * 100}%`,
}}
>
<div
{...thumbProps}
onMouseDown={(...args) => {
thumbProps.onMouseDown(...args)
props.onChangeStart?.()
}}
onPointerDown={(...args) => {
thumbProps.onPointerDown(...args)
props.onChangeStart?.()
}}
className={clsx(
'h-4 rounded-full',
isFocusVisible || state.isThumbDragging(index)
? 'w-1.5 bg-slate-900'
: 'w-1 bg-slate-700'
)}
>
<VisuallyHidden>
<input ref={inputRef} {...mergeProps(inputProps, focusProps)} />
</VisuallyHidden>
</div>
</div>
)
}
export function Slider(props) {
let trackRef = useRef(null)
let state = useSliderState(props)
let { groupProps, trackProps, labelProps, outputProps } = useSlider(
props,
state,
trackRef
)
let { focusProps, isFocusVisible } = useFocusRing()
let currentTime = parseTime(state.getThumbValue(0))
let totalTime = parseTime(state.getThumbMaxValue(0))
return (
<div
{...groupProps}
className="absolute inset-x-0 bottom-full flex flex-auto touch-none items-center gap-6 md:relative"
>
{props.label && (
<label className="sr-only" {...labelProps}>
{props.label}
</label>
)}
<div
{...trackProps}
onMouseDown={(...args) => {
trackProps.onMouseDown(...args)
props.onChangeStart?.()
}}
onPointerDown={(...args) => {
trackProps.onPointerDown(...args)
props.onChangeStart?.()
}}
ref={trackRef}
className="relative w-full bg-slate-100 md:rounded-full"
>
<div
className={clsx(
'h-2 md:rounded-r-md md:rounded-l-xl',
isFocusVisible || state.isThumbDragging(0)
? 'bg-slate-900'
: 'bg-slate-700'
)}
style={{
width:
state.getThumbValue(0) === 0
? 0
: `calc(${state.getThumbPercent(0) * 100}% - ${
isFocusVisible || state.isThumbDragging(0)
? '0.3125rem'
: '0.25rem'
})`,
}}
/>
<Thumb
index={0}
state={state}
trackRef={trackRef}
onChangeStart={props.onChangeStart}
focusProps={focusProps}
isFocusVisible={isFocusVisible}
/>
</div>
<div className="hidden items-center gap-2 md:flex">
<output
{...outputProps}
aria-live="off"
className={clsx(
'hidden rounded-md px-1 py-0.5 font-mono text-sm leading-6 md:block',
state.getThumbMaxValue(0) === 0 && 'opacity-0',
isFocusVisible || state.isThumbDragging(0)
? 'bg-slate-100 text-slate-900'
: 'text-slate-500'
)}
>
{formatTime(currentTime, totalTime)}
</output>
<span className="text-sm leading-6 text-slate-300" aria-hidden="true">
/
</span>
<span
className={clsx(
'hidden rounded-md px-1 py-0.5 font-mono text-sm leading-6 text-slate-500 md:block',
state.getThumbMaxValue(0) === 0 && 'opacity-0'
)}
>
{formatTime(totalTime)}
</span>
</div>
</div>
)
}

@ -0,0 +1,296 @@
1
00:00:02,790 --> 00:00:51,090
Thomas Hintz: Welcome to the React show brought to you from occupied Lenape territory by me, your host, Thomas, and some funny code, Episode 73. Hooks in functional components changed react in a significant way compared to class based components. But I think react server components are going to alter react in an even more fundamental way. Hooks allowed us to basically just do what we were already doing, but in a different fashion. React server components, though, completely changed the way in which we'll create and even think about React apps. In this episode, we start learning what react server components really are, and take a journey attempting to use them in the real world.
2
00:00:53,010 --> 00:01:34,170
Thank you for joining us, I've been following the development of React server components sort of like in the background for the last year or so. And even in the episode where we covered React 18 And talked about React server components, I did start to get excited. But even then, you still couldn't really get a good feel for what they would be like to use in practice as the only things really released until that point, were more like proofs of concepts, you know, from the React team, and from NextJS and stuff. So I was just thinking these seem like they have a lot of potential, but who really knows until we see a more finalized version?
3
00:01:34,710 --> 00:01:53,910
Well, with NextJS 13, I think we do get to see a much more finalized version. Technically, React server components are still in beta. But it's almost certain the final version will be very similar to what has been released in NextJS 13. So we can finally take them for a spin.
4
00:01:53,940 --> 00:02:11,130
And I'll be honest, I have been and I've been pretty blown away. It's not that it's really anything new in terms of like programming or web development. But it's completely new for React. And in my experience, so far, just, it's just completely game changing.
5
00:02:11,700 --> 00:02:43,650
And yeah, for some extra fun, after recording this main episode, I'm going to do a short after show, recording, that just covers some more of my thoughts about the future of react. And you know what that might be with React server components, just in a more casual fashion, some cool tricks we can bring in from other systems that will be sort of like steroids for React server components, along with some interesting tidbits I picked up from my research, like how the React team actually looked into making react more like Svelte.
6
00:02:44,160 --> 00:02:54,960
So yeah, if you're interested in just sort of joining along for that more casual follow up, the after show will be available exclusively on our Patreon. So definitely check that out. If you're interested.
7
00:02:55,560 --> 00:03:06,720
Before we actually get into what react server components are, I thought it would be more digestible, if I like sort of walked you through some of my experiments with React server components.
8
00:03:07,680 --> 00:04:12,240
First, I wanted to build a real sort of like a real world app using React server components and NextJS 13. And everything to get, like a good feel for what they were like and how to use them and stuff. So my goal was to just build the simple weather app, you know, where it lists some days, and each day shows like a summary of the temperature range or something, and then you can maybe click on a day, and it'll like, bring up more details, you know, things like that. Basically, I just wanted something that, you know, depended on some sort of external API. So I could see how data fetching and data integration worked. But also had, you know, some client side state, some things I wanted to have real interactivity with that, you know, needed to happen quickly to feel responsive. So I can just sort of get a feel for, you know, the sort of boundary between client and server and what it means to have some, maybe, client component versus, say, a server component. So yeah, it's pretty loose goal. But yeah, that was the goal, create this weather app.
9
00:04:12,240 --> 00:04:47,550
So the first task, of course, was getting NextJS 13, with this new beta React server components support up and running. So in NextJS, they often call this app like just a-p-p support, because this new system lives inside the App folder instead of the Pages folder. So previously, with NextJS, you put your pages in the Pages folder, now you basically put your pages in the App folder. And once you do that, you can take advantage of all these new features.
10
00:04:47,850 --> 00:05:21,030
So yeah, I got an NextJS, and I passed a flag to it, telling it to use the new beta app support and I got that up and running and didn't have any issues. You know, so this included a server component and some client components in sort of like a, you know, demo app that includes with it, and I got that up and running and no issues there. So that was that was really cool. Just worked. And yeah, it was fun to see this, you know, in practice and then actually going. So of course, next I had to like, actually do something with this, right.
11
00:05:21,690 --> 00:05:45,870
So I just created a basic server component, like I read through the docs and learn how to create a server component how to create a client component. By default, all the components are actually server components, which is different. But you can put a little tag basically at the top of your file to indicate it should be a client component. And so I created a server component that rendered a both the client component and a server component.
12
00:05:46,140 --> 00:06:10,410
And it just worked. I had no issues, it was really cool. It was really exciting to see it was like, Oh, this works. You know, I didn't really take advantage of any special features that I knew of at this point. But hey, it's working. So that's great. And now that I had something working, it was like, Okay, let's just sort of do some investigation, like, what is Next doing, like, what is different than before? Right?
13
00:06:10,830 --> 00:06:47,610
So I started investigating, and it was pretty clear that, you know, Next is still providing server side rendering, which is different than react server components. It can still be used with React server components, but Next has done server side rendering for quite some time. And that's been supported on React for a while. And that seems to just work. Like before it does server side rendering, it returns HTML, and that gets hydrated by react. So that seems the same. But then, and this is maybe also, in a sense, not new: Next has done some of this as well before.
14
00:06:48,030 --> 00:07:12,900
But I had link to another page, and it seemed to prefetch, this linked page, but that's where things started to get more interesting. So I, you know, brought up the network inspector, right? And I was like, okay, cool. It's like prefetching, you know, this other page. But then when I was looking at what it was actually prefetching, I was like, Whoa, this is totally different. This is super interesting.
15
00:07:13,410 --> 00:08:00,000
So the result, you know, what the next server was returning to the client, it basically, it was data. It wasn't HTML, it was just data. But it looked to me, like the output from rendering a React tree, like, like the actual tree structure itself. And it was really, it's actually really funny. So if you are familiar with my book at all, where we like, create our own react, I create sort of my own data structure to represent the tree that the React rendering algorithm can parse to do its thing, right? This data that the server is sending to the client looks basically identical to that. I mean, there's only minor differences. So that was really fun to see. Yeah, it's basically the same thing.
16
00:08:00,000 --> 00:08:18,660
It's like, Yeah, this is the data that represents a React tree of components, I guess. So it's not surprising, it would look just similar the same, but it was really fun to see. So yeah, this is it. We're starting to get into "this is different, this is cool, this is unique, this is, this is fun!" At least it was fun for me. Anyway.
17
00:08:18,660 --> 00:09:16,920
So I'm like going through this investigating things, I noticed a couple other things. One is it seemed like, like some of these requests, were being streamed in, potentially, like just looking at the HTTP requests and the browser and stuff. I was like, Oh, it seems like React is doing some sort of, or Next or something is doing some sort-of streaming, which I hadn't seen in this fashion before. That's cool. And a really cool thing is, it seemed like React was doing some sort of automatic code splitting based on component like, it looks like, oh, it's not even sending the code it needs for a component, unless the user actually is going to load the page with that component on it. Like, I don't know exactly what it's doing yet, or how it's working but I was like, hey, that seems really cool. I definitely want to look into that further, you know, so I had some, you know, fun inspecting what was going on?
18
00:09:16,920 --> 00:09:26,520
And I think I came up with a lot of questions like, you know, what is it doing? But it was really exciting. And, yeah, so I think we'll definitely learn more about that.
19
00:09:27,030 --> 00:09:50,970
But the next thing that I wanted to mess around with was, like data and data fetching. So to me, based on the research I had done, this seemed like potentially one of the biggest places where things changed or could change, or we could do things differently, you know, so I wanted to fetch data within a server component and just sort of get a feel for how that worked. Right?
20
00:09:51,270 --> 00:10:32,400
So I follow the NextJS docs to use a basic await fetch call to get some weather data from an API inside my react server component, and it just worked and it was awesome! So much better than having to use React query or doing it manually via useEffect, which React Query is almost certainly doing internally, right? It was just so beautiful and so simple. Like, it was just like, oh, 'await' the results of my API call and render them out in my component, I don't need to do anything special, you know, I don't need to follow these useEffect rules or something like that, you know. So yeah, this is super exciting.
21
00:10:32,930 --> 00:11:13,160
When this worked, and I could sort of play with it and see how it worked. I was like, this has the potential to just fundamentally change how I write React apps. So anyways, this is the journey so far I created. It's not really the Weather app completely yet. But it fetches some weather data. And it just sort of renders it in a list to the screen to the browser's DOM, you know, and it's cool and all, but now it's like, Hey, I got an idea how this kind of works. But what actually are, you know, react server components, I wanted to get more of the theory, you know, so yeah, we'll talk about that.
22
00:11:13,160 --> 00:11:46,730
Next, like I mentioned earlier, I guess, we've been able to render a tree of React components on the server for quite some time, you know, we've called it server side rendering, this worked by providing the tree of components with some default state and props, and just rendering basically the same as on the client, except for we just take the HTML output, and, you know, throw it to the browser. And that output, you know, gets hydrated by react, you know, where it just like, adds event and listeners and stuff to the DOM elements, right?
23
00:11:47,210 --> 00:12:53,750
Well, for React server components, we actually basically do the same thing, except instead of only being able to send the HTML output to the client, we instead send some JSON output that contains the data for the tree of React components. Essentially, if you're familiar with the term virtual DOM, we're essentially sending the virtual DOM from the server to the client. And then on the client side, React has enough information to merge this, like virtual DOM that it got from the server with whatever the state of the virtual DOM is on the client. So really, we used to have two trees, essentially a React component data, the output from rendering your components is incorporated into the DOM elements on the screen, right? But also, whenever you do a rerender, you generate like a new tree. And React is taking that, you know, those two trees, and essentially merging them together to figure out what needs to get updated on the screen. Right, so you had two trees.
24
00:12:54,200 --> 00:13:17,840
But now we have three trees, the server tree, the client tree, and the browser DOM tree. So all we actually need to do is merge these three trees together. And this is what React does for us. This is basically the new feature they added for React server components is the ability to merge these three trees together.
25
00:13:18,470 --> 00:13:29,360
Of course, it's not completely that simple. There are some distinctions between, essentially the client tree and the server tree or client components and server components.
26
00:13:29,000 --> 00:13:50,180
On the client side, nothing really changes at all, we still have access to all the same hooks and all the same features. Basically, it's completely backwards compatible. When it comes to client components, which are the components we always used to write, they're now called Client components. Basically, nothing changes there.
27
00:13:50,450 --> 00:14:24,740
However, server components, like they look the same, you still write a function that returns JSX, or whatever, it looks basically the same, and works basically the same, but there are some limitations. So on server components, you can't use anything that has to do with client side state or, of course, browser specific API's. So this means you can't use useState or useEffect or useContext or anything like that in your server components.
28
00:14:25,430 --> 00:15:01,160
So this actually means that probably a lot of your components can be either a client component or a server component. And in some cases, you can probably use them as client components in some parts of your app. But in other parts, use them as server components, like the code in a lot of cases is fine to run in both environments, so as long as you're not using client side specific stuff, like useState or you know window or other browser specific API's, you can you is it in both places.
29
00:15:01,400 --> 00:15:13,280
Now, of course, there are some things that you can do in a server component, which is really just code running on node. JS, that you can't do on a client component, which we're going to get into as well.
30
00:15:13,660 --> 00:15:40,930
But what is really, really cool about all of this is that the server and client trees can be merged together without affecting client side state. So while you can't access or modify client side state from within a server component, you can fetch data in Server components, which will get merged with the client components without causing a reload or any loss of client side state.
31
00:15:41,350 --> 00:16:08,710
To me, this last point is really the key to server components. They allow you to fetch, load and process data in a much more natural and efficient way on the server, while still having a highly responsive client with all the same client interactivity you would want in an app. Ultimately, with web apps, we always have to deal with clients and servers and having the power to choose where our React code runs, is incredibly powerful.
32
00:16:09,040 --> 00:16:25,630
Choice, and power is what makes react so good, in my opinion. And this just extends and doubles down on this. So amazing, super exciting, in my opinion. And we're definitely going to get more into some of the details around this.
33
00:16:25,630 --> 00:17:01,060
But what I also found really interesting to me is that the fundamentals of how React really like works haven't changed. Like the way react renders stays the same, like it renders in two different environments now, but the algorithm is the same. There are some restrictions on some features, depending on the environment, like server or client, but the fundamental algorithm react to uses internally remains the same. So yeah, if you're interested in the nitty gritty details of the algorithm, I did write a short book on the topic, which covers it in depth.
34
00:17:01,330 --> 00:17:16,720
But it was just really cool to be like, wow, they designed this thing that works in both of these environments without really having to change anything about the fundamental way React works. I don't know, to me, that's really cool. And indicates like good design.
35
00:17:17,470 --> 00:17:40,390
Anyways, so back to my journey, trying to build this app. So this like weather app, or whatever. So I got some basic stuff working. But now I really wanted to try to get a more full fledged app developed and try to really exercise the full capabilities, like kind of figure out the limitations and how to do things in this new world, right?
36
00:17:40,840 --> 00:17:58,030
So the next experiment was adding some client side interactivity. And it's kind of not related to a weather app anyway. But I was like, hey, let's add a button with a counter, you know, where you click, every time you click the button, the counter on your screen goes up by one, right, just some text on the screen.
37
00:17:58,240 --> 00:18:38,980
Just super basic, but you know, it's client side state, I was going to maintain all this data on the client, and I wanted to be like, okay, when I send new data, you know, from the weather API back to the client, or whatever, does this erase this client side state? Does it impact it in any way? Like, what is the effect here, you know, so that meant I had a server component that was rendering both another server component and a client component. And it all just worked seamlessly. Like, to be completely honest, if you didn't know any better, you couldn't even really tell that I was using some new server component technology. It just worked. It didn't lose it state, everything was great.
38
00:18:39,050 --> 00:18:59,120
So next, I thought I wanted to, like make sure that whatever day I clicked on in this, you know, list of days for the weather forecast, I wanted it to be highlighted, and like open up in another pane or something, some extra details about the weather for that day, you know, and this is where things started to get really interesting.
39
00:18:59,570 --> 00:19:34,250
So to do this, I needed to have an onclick handler, when I click on this day, which I think was a div or whatever, and then an onclick handler to fetch more information about that day and display it and highlight that day in the list of days, you know, but event handlers can only be specified within client components. But the component that I created to render the list of days was a server component. And it's like, if you try to add an event handler to it, React is just like, nope, what are you doing? You don't--you're out to lunch, you know, like, this doesn't make any sense.
40
00:19:34,700 --> 00:20:03,830
So somehow, I need to figure out a way to pass data to that server component to like, tell it which day was selected, like I need to create, I guess, a client component for each day in that list that had the onClick handler. And then when you click on it, I needed to somehow tell the server, which day was clicked, so that server component could render some CSS or something to show that day was highlighted like to show I clicked on that day, right?
41
00:20:04,310 --> 00:20:40,040
Of course, I could have done all of this as client components, you know, the way we used to do it in React. But that's no fun. That's not the point of this, right? I wanted to have this as a server component and just see how it worked. But you can't use the useState hook within the server component. You can't use the useContext hook, right? So I thought, if I made a context higher up in my tree, right, so this is not new in React, we create contexts that allow us to share data throughout our tree without having to pass that data down through the tree, right?
42
00:20:40,400 --> 00:21:02,270
So I was like, Okay, I could create a context higher up in the tree, in a client component, that client component could render my server component that renders the list of days. And that would, again, render some client components. And I could use this to share data between all my components, right. But again, this turned out to not really work.
43
00:21:02,330 --> 00:21:39,110
So server components, they just can't access the data that's stored in a context, only client components can. So basically, I realized that I could pass state and props like normal between client components, and even from a server component to a client component. But it didn't seem like there was any way to pass props, or a function from a server component to a client component. So basically, I was stuck. I was like, I don't know how to make this work. Like how do I tell the server which day is selected?
44
00:21:39,620 --> 00:22:01,430
So I took a timeout to study what the React team did in their demo for React server components that they created in like 2020, the end of 2020. So at first, like, I was looking through their code, and I was like, Oh, they are using a context. So I must be able to use a context to do this. Like it looked like they were using it to pass data between client and server components.
45
00:22:01,690 --> 00:22:34,630
But eventually, I figured out that the context was really basically just acting as a client-side cache, the real way they were passing data from client components to server components was via query parameters. Ah, I had figured it out! It was so exciting! So basically, you just can't directly pass props or state from a client component to a server component. So you have to pass data in some way that a server can read it.
46
00:22:35,290 --> 00:22:52,120
There's multiple ways you could do this, you know, whether that's just a regular API call that you make to your server that stores the data in a session or wherever, right? Or, in this case, via the URL, which a server can obviously, you know, access from any request that comes in.
47
00:22:52,120 --> 00:23:30,640
So basically, the way it works is you're like, hey, render the server component. And on the server, I can be like, Oh, okay, I will do that, but what URL did you ask for the server component on, and you can use that to like, set the query parameters on that URL and pass data from the client component to the server component. It worked, and it worked really well. And the cool thing was, I could directly pass data via props, from a server component to a client component. And the NextJS like, API allows directly accessing the data in query parameters.
48
00:23:31,060 --> 00:23:55,150
I know this probably sounds confusing, trying to explain it in a podcast. But the bottom line is, once I realized that and took advantage of the existing, you know, NextJS API's, it all just worked seamlessly, I could pass data from the client to my server components, and back from my server components to my client components, all pretty much seamlessly.
49
00:23:55,990 --> 00:24:19,480
The way I did this was I used the NextJS link components. And I used those to set the query parameters in the client components. So the client component, you know, would render and set up each of those link components with different query parameter values. And so when you clicked on those links, the server would get the unique data from the query parameter and be able to use it.
50
00:24:19,660 --> 00:25:02,050
So now I could pass the data from the client to the server and back to the client, again, all quite seamlessly. Like I know, I know, this is not groundbreaking, we've been able to pass data from the server to the client back and forth, again for the ages. So you know, like, that's how you do things on the web, right? But the way in which you can pass them and render the data and the output of these components in like sort of interleave your server and client components and just sort of write components and think in terms of components. I don't know it was just mind blowing. It's totally different than the way we've ever done things in the React World, and just really cool in my opinion.
51
00:25:02,630 --> 00:25:19,490
So with this basic prototype setup, I could really start experimenting. I also like I don't know, wanted to add a form, I've always just sort of been obsessed with trying to find a better way to do forms in React. And so I was like, Hey, maybe, this will let me do it.
52
00:25:19,490 --> 00:25:38,540
Right, so, again, it's not really related to a weather app, per se, but I was like, I could add a form that lets you select a day using an input element, you know, and then it would show that day, I don't know why you'd really--I guess you could want this on a weather app, whatever, I wanted to add a form and just see how it works.
53
00:25:38,540 --> 00:26:26,450
Right, so I passed some data from the higher level server component down into the form components, this turned out to be really awesome, because I figured out that you could update data, within the form, via server components without clearing the state of the form. So you could have some client components in your form for some of your inputs, you know, so it's like some custom made highly interactive form inputs, right? Then you can mix that with some server components that fetch data in real time or update in real time or update as the user is filling out the form even. And it doesn't, you know, erase the state of the form from the browser's perspective, and also from React's perspective.
54
00:26:26,750 --> 00:26:45,200
So I don't, I need to do some more research in this area to figure out, you know, what the full implications of this are, but to me, this seemed like a potentially a huge breakthrough in the way we could do forms and React. So yep, super exciting it was really fun! I really enjoyed messing around with this!
55
00:26:45,830 --> 00:27:01,040
So when I was reading through the Next documentation, for React server components, they also mentioned that you can use Suspense boundaries and transitions to create loading states. So I messed around with that, too. And it worked really well.
56
00:27:01,000 --> 00:27:41,110
So you can fetch data in your server component, right. But you can use suspense to return a loading state. And so on the client side, it will show this loading state until the data returns on the server side, but you don't need to, like do any special coding, you just await your data and, you know, render a Suspense element in your output. And I'll just work seamlessly. It was like, I finally understand suspense boundaries. Like I mean, I always understood them before, but they were really clunky, to be honest, on the client side to coordinate and make work. And it works really seamlessly with server components: really awesome!
57
00:27:41,710 --> 00:28:00,700
Another fun thing I realized is that in a React server component, you can, you know, have an 'if' statement, or some sort of branching. And, you know, maybe depending on if the users logged in or not, or some other condition, you can return one component or a different component, right?
58
00:28:01,060 --> 00:28:31,600
Well, before all of the code needed to render both components would get sent to your browser, whether the user ever actually needed all of the components or not, it always got sent to the browser. Well, I started messing around with it. I was like, Oh, this is really cool: if I've branching code in my output, React only actually sends the code needed for client side components, if they are actually getting used. So that was really fun to mess around with as well.
59
00:28:32,110 --> 00:29:04,510
All right, so I have a really ugly but functioning weather app at this point that has a mix of server components and client components. And it's really cool, and really fun and quirky, and not that useful in the real world yet, cuz I didn't polish anything but hey, it was a fun experiment, right? But I thought, alright, so we've got that working, I think it's a good time to sort of regroup on what we've learned and also just talk about and expand upon some of the new capabilities that NextJS 13 and React server components brings.
60
00:29:05,020 --> 00:29:26,410
So like I said, before, server components, I think, fundamentally change how we're going to fetch data in React. The NextJS documentation even recommends only fetching data in Server components. This could mean that days of useEffect for data fetching and even things like React Query are maybe over.
61
00:29:26,590 --> 00:29:43,060
We might not actually need React Query or SWT, or anything like that ever again. And that remains to be seen. Maybe there are still use cases for it. But in scenes like that, you know, we're going in a different direction now.
62
00:29:43,660 --> 00:30:01,330
So the React team, to sort of, kick off the whole React server components project, they created an RFC "request for comment" about their ideas, and I thought it covered a lot of the benefits of React server components really well.
63
00:30:01,330 --> 00:30:38,470
So I'm going to go over what they present in the RFC. So server components, they run only on the server, and have zero impact on bundle size. Their code is never downloaded to clients, which reduces bundle size, improves startup time, like, for example, let's say you wanted to render a date to the screen in a nice, pretty format that your users like to look at, not whatever, you know, milliseconds since 1970, or whatever format your data is stored on in your API or in your database or whatever.
64
00:30:38,470 --> 00:31:29,350
Right. So normally, we would include a date library, like, you know, there's many of them, we've included on the client side and use that to tell it what format to convert the date to for the user, right? Well, that sucks, because now the user has to download that date library. With React server components, you can still use that exact same date library to format your dates. But you never need to send the code for the date library to the clients. It's awesome. Server components can also access server side data sources directly, such as databases, file systems, micro services, you don't need to do anything special, you can just access files, the contents of files, create database connections, all within your React server components!
65
00:31:29,830 --> 00:32:15,760
Another cool feature is server components seamlessly integrate with client components. So I talked about this before. So server components can load data on the server and pass it as props to client components, allowing the client to handle rendering the interactive parts of a page. And server components can dynamically choose which client components to render, so I was talking about this at the end: essentially, they allow clients to download just the minimal amount of code necessary to render a page. Server components preserve client state when reloaded. This means that client state focus and even ongoing animations aren't disrupted or reset when a server component tree is refreshed. Amazing. Mind blown.
66
00:32:17,070 --> 00:32:37,860
Yeah, also, server components are rendered progressively and incrementally stream rendered units of the UI to the client, kind of a cumbersome way of basically saying, as React renders your component tree, it can start streaming the results to the client before it finishes rendering all of your components.
67
00:32:38,370 --> 00:33:03,210
And combined with Suspense, this allows developers to craft intentional loading states and quickly show important content while waiting for the remainder of a page to load. And yeah, I could see in practice, when I was trying this out, it works way better than the way we used to have to do it in React, really cool! And, you know, something a lot of people have tried to replicate in lots of different ways.
68
00:33:03,780 --> 00:33:29,790
We can now share code between the server and the client as React components, allowing a single component to be used to render, say, a static version of some content on the server on one route, and an editable version of that content on the client and say, a different route. And I think there will ultimately be even more benefits realized by React server components than just those we listed above. And you know, time will tell.
69
00:33:30,110 --> 00:34:25,100
But this isn't like necessarily for free, there are some downsides. For one, it's just more complicated, the React ecosystem is absolutely going to be more complicated. Learning React is going to be harder than it was before when we only had client side components. But at the same time, I think the payoff is definitely worth it unless you only ever planned on making purely client side apps that never need API or data integration. But if you're not in that super rare case, I think if you're just writing normal apps, where you are going to need API and data integration, all this, you're gonna have to learn how servers and clients work anyways, you're gonna need a server, you're you know, so I don't think it makes it so that overall, you really need to learn more, but learning React, this is probably going to make it so you got to learn more before you really can say I've learned React, you know, and there are definitely some other things that still need to be worked out.
70
00:34:25,100 --> 00:34:44,870
So what I discovered when I was, you know, trying to build this weather app is that a lot of component libraries, you know, like Chakra UI, Material UI, and also CSS and JS libraries, they don't really work with server components right now, or at least don't work effectively.
71
00:34:45,080 --> 00:35:24,080
You can like technically with some extra work, get them to work on your client components, but I found they basically just don't have an effect on your server components which made them really kind of a pain and kind of useless to use. And I'm sure there's going to be other growing pains, but I'm sure we're going to figure it out, you know, whether it's using different techniques or these libraries, maybe they just need to be updated, whatever. But at this point in time, there's definitely issues with a lot of existing React libraries. It's definitely not production ready right now.
72
00:35:24,000 --> 00:35:49,530
But I had a blast trying it out, if you couldn't tell. So I'm curious though, what do you think? Are you wanting to try it out now? Is there anything I didn't mention that you want to experiment with? Do you want me to experiment with? Or does this just make you angry, you want to go back to a world where everything is client side and seemed simpler? Either way, like always, we'd love to hear from you.
73
00:35:49,590 --> 00:36:04,800
And, you know, if you're curious about some of my more off the cuff, remarks and thoughts about React server components and some of my other ideas on really cool things we could do with them, definitely check out the After Show on our Patreon.
74
00:36:05,250 --> 00:36:18,780
And like always, we hope you have a great rest of your day. Thank you so much for joining us. This episode was produced by Thomas and edited by Dougie, The Podcast Editor.

@ -0,0 +1,18 @@
Getting Started With Your Own Editor
To actually get started with your own project install next.js
install node
run command: npm install react react-dom next
run command: npx create-next-app [name-of-project] --use-npm
run command: cd [name-of-project]
run command: npm run dev
this will open a browser window with your running react site
Then you can use this project as a sandbox to learn React or just to get started building a new project.
open pages/index.js
delete everything between the parens after return
you can now write your react code here
you can declare new components in this file too and use them

@ -0,0 +1,197 @@
import * as srtparsejs from "srtparsejs";
import fs from 'fs'
import { extractFromXml } from '@extractus/feed-extractor'
const episodeExtra = {
'Buzzsprout-12076221': {
slug: 'a-fundamentally-new-react-my-journey-with-react-server-components',
transcript: srtparsejs.parse(fs.readFileSync('./src/data/Buzzsprout-12076221.srt').toString())
},
'Buzzsprout-12033274': {
slug: 'learning-react-on-only-3-hours-per-week-while-working-full-time'
},
'Buzzsprout-11941927': {
slug: 'testing-useeffect-porting-rn-app-to-web'
},
'Buzzsprout-11918765': {
slug: 'react-2022-year-in-review-foundational-changes'
},
'Buzzsprout-11912454': {
slug: 'news-dec-21st-chatgpt-swr-20-wasp-mfa-ci-react-visual-cms-flash-in-2022'
},
'Buzzsprout-11879575': {
slug: 'how-i-built-my-own-react'
},
'Buzzsprout-11802072': {
slug: 'faq-typescript-svelte'
},
'Buzzsprout-11802002': {
slug: 'thinking-in-react'
},
'Buzzsprout-11757420': {
slug: 'how-decentralized-is-crypto-really'
},
'Buzzsprout-11683392': {
slug: 'concise-ish-beginners-guide-to-learning-react' // TODO extra content
},
'Buzzsprout-11586984': {
slug: 'its-not-your-fault-you-dont-understand-the-code'
},
'Buzzsprout-11533367': {
slug: 'your-boss-is-wrong-and-how-slow-is-react'
},
'Buzzsprout-11500932': {
slug: 'the-reality-of-micro-frontends-and-why-i-dont-recommend-them'
},
'Buzzsprout-11235167': {
slug: 'react-faq'
},
'Buzzsprout-11235154': {
slug: 'remix-as-fast-as-instant'
},
'Buzzsprout-11193020': {
slug: 'noobs-vs-experts-with-kyle-verhoef'
},
'Buzzsprout-11130436': {
slug: 'oops-i-built-the-wrong-thing'
},
'Buzzsprout-11086813': {
slug: 'a-new-react-compiler'
},
'Buzzsprout-10735883': {
slug: 'forms-still-suck-can-we-design-something-better'
},
'Buzzsprout-10402825': {
slug: 'how-to-build-react-features-right-the-first-time'
},
'Buzzsprout-10365089': {
slug: 'why-react-should-die'
},
'Buzzsprout-10317720': {
slug: 'how-javascript-actually-executes'
},
'Buzzsprout-10278986': {
slug: 'whats-the-hype-about-shopify-hydrogen'
},
'Buzzsprout-10229771': {
slug: 'preventing-startup-burnout-with-brian-wetzel-part-2'
},
'Buzzsprout-10181548': {
slug: 'preventing-startup-burnout-with-brian-wetzel-part-1'
},
'Buzzsprout-10138345': {
slug: 'taking-the-pain-out-of-forms-in-react'
},
'Buzzsprout-10037837': {
slug: 'what-are-react-server-components-and-why-theyre-awesome'
},
'Buzzsprout-10037718': {
slug: 'react-fibers-how-your-app-actually-executes'
},
'Buzzsprout-10011866': {
slug: 'how-to-successfully-test-react-apps-using-cypress'
},
'Buzzsprout-9935994': {
slug: 'chris-keen-on-succeeding-as-a-react-contractor'
},
'Buzzsprout-9926848': {
slug: 'query-caching-why-you-must-use-it-with-react-using-react-query'
},
'Buzzsprout-9886172': {
slug: 'where-and-how-to-store-data-from-your-react-application'
},
'Buzzsprout-9842114': {
slug: 'how-to-stop-wasting-your-time'
},
'Buzzsprout-9811470': {
slug: 'react-component-lifecycle-what-is-a-component'
},
'Buzzsprout-9781457': {
slug: 'why-you-need-to-check-software-licenses'
},
'Buzzsprout-9740045': {
slug: 'alternatives-to-the-software-interview-getting-a-react-job'
},
'Buzzsprout-9698201': {
slug: 'what-do-you-think-of-react-and-other-qa'
},
'Buzzsprout-9656919': {
slug: 'refactoring-quickly-safely-and-easily'
},
'Buzzsprout-9608790': {
slug: 'how-to-diagnose-react-app-bottlenecks-with-the-profiler'
},
'Buzzsprout-9545960': {
slug: 'so-where-do-you-host-your-react-app'
},
'Buzzsprout-9502941': {
slug: 'is-your-react-app-killing-the-planet'
},
'Buzzsprout-9464470': {
slug: 'better-routing-in-react-with-nextjs'
},
'Buzzsprout-9451117': {
slug: 'debug-smarter-in-your-react-apps'
},
}
const slugToEpisode = {}
Object.entries(episodeExtra).forEach(([id, { slug }]) => {
slugToEpisode[slug] = id
})
export async function getEpisodes() {
const feedRes = await fetch('https://feeds.buzzsprout.com/1764837.rss', { next: { revalidate: 60 * 10 } });
const feedString = await feedRes.text()
/* const feedString = fs.readFileSync('./feed.rss').toString() */
let feed = await extractFromXml(feedString,
{
getExtraEntryFields: (feedEntry) => {
const {
enclosure
} = feedEntry
return {
enclosure: {
url: enclosure['@_url'],
type: enclosure['@_type'],
length: enclosure['@_length']
},
content: feedEntry['content:encoded'],
chapters: feedEntry['podcast:chapters'] && feedEntry['podcast:chapters']['@_url']
}
}
})
return feed.entries.map(
({ id, title, description, enclosure , published, content, chapters }) => ({
id,
title,
published,
description,
content,
chapters,
slug: episodeExtra[id]?.slug || title.replace(/[\W_]/g, '-'),
transcript: episodeExtra[id]?.transcript,
audio: [enclosure].map((enclosure) => ({
src: enclosure.url,
type: enclosure.type,
}))[0],
})
)
}
export async function getEpisode({ episodeSlug }) {
const episodes = await getEpisodes()
const episodeId = slugToEpisode[episodeSlug] || episodeSlug
let episode = episodes.find(({ id }) => id === episodeId) ||
episodes.find(({ title }) => title.replace(/[\W_]/g, '-') === episodeId)
if (!episode) {
return {
notFound: true,
}
}
return episode
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@ -0,0 +1,5 @@
* TODOs
** TODO contact us email sending
** TODO book stripe integration
** TODO PDF download not working
** TODO favicon

@ -0,0 +1,23 @@
const defaultTheme = require('tailwindcss/defaultTheme')
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,jsx}'],
theme: {
extend: {
fontFamily: {
sans: ['Satoshi', ...defaultTheme.fontFamily.sans],
},
spacing: {
18: '4.5rem',
112: '28rem',
120: '30rem',
},
},
},
plugins: [
require('@tailwindcss/line-clamp'),
require('@tailwindcss/typography'),
require('@tailwindcss/forms'),
],
}
Loading…
Cancel
Save