Initial commmit.

This commit is contained in:
2023-01-30 08:27:08 -08:00
commit 15257b917a
66 changed files with 15562 additions and 0 deletions

View File

@@ -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"
/>
);
};

View File

@@ -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>
</>
)
}

View File

@@ -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."
/>
);
};

View File

@@ -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>
</>
)
}

View File

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

View File

@@ -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>
);
}

View File

@@ -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."
/>
);
};

View File

@@ -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>
);
}

10
src/app/(main)/head.jsx Normal file
View File

@@ -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."
/>
);
}

280
src/app/(main)/layout.jsx Normal file
View File

@@ -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>
);
}

View File

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

245
src/app/(main)/page.jsx Normal file
View File

@@ -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>
</>
)
}

View File

@@ -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. "}]}

View File

@@ -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}
/>
);
};

View File

@@ -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>
</>
)
}

52
src/app/Player.jsx Normal file
View File

@@ -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>
);
}

3
src/app/globals.css Normal file
View File

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

10
src/app/head.jsx Normal file
View File

@@ -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" />
</>
)
}

19
src/app/layout.jsx Normal file
View File

@@ -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>
);
}

271
src/app/page.module.css Normal file
View File

@@ -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);
}
}