Refactorings.
This commit is contained in:
1559
package-lock.json
generated
1559
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,9 @@
|
|||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-stately": "^3.17.0",
|
"react-stately": "^3.17.0",
|
||||||
"sanitize-html": "^2.8.1",
|
"sanitize-html": "^2.8.1",
|
||||||
"srtparsejs": "^1.0.8"
|
"sqlite3": "^5.1.4",
|
||||||
|
"srtparsejs": "^1.0.8",
|
||||||
|
"stripe": "^11.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
|
|||||||
7
src/app/(main)/episodes/page.jsx
Normal file
7
src/app/(main)/episodes/page.jsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import Episodes from '@/components/Episodes'
|
||||||
|
|
||||||
|
export default async function AllEpisodes() {
|
||||||
|
return (
|
||||||
|
<Episodes />
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,175 +1,7 @@
|
|||||||
export const dynamic = 'force-dynamic'
|
|
||||||
|
|
||||||
import { Suspense } from "react";
|
|
||||||
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
import { ArrowLongLeftIcon, ArrowLongRightIcon } from '@heroicons/react/20/solid'
|
|
||||||
|
|
||||||
import { Container } from '@/components/Container'
|
import { Container } from '@/components/Container'
|
||||||
import { FormattedDate } from '@/components/FormattedDate'
|
import Episodes from '@/components/Episodes'
|
||||||
|
|
||||||
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 }) {
|
function Skeleton({ width, height, className, color }) {
|
||||||
const w = width ? '' : 'w-full';
|
const w = width ? '' : 'w-full';
|
||||||
@@ -229,19 +61,18 @@ function Loading() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Home({ searchParams }) {
|
export default async function Home() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="pt-16 pb-12 sm:pb-4 lg:pt-12">
|
<Episodes limit={15} />
|
||||||
<Container>
|
<Container>
|
||||||
<h1 className="text-2xl font-bold leading-7 text-slate-900">
|
<Link
|
||||||
Episodes
|
href="/episodes"
|
||||||
</h1>
|
className="flex items-center text-md font-bold leading-6 text-pink-500 hover:text-pink-700 active:text-pink-900"
|
||||||
</Container>
|
>
|
||||||
<Suspense fallback={<Loading />}>
|
View All Episodes
|
||||||
<Content page={searchParams?.page ? parseInt(searchParams?.page, 10) : 1} />
|
</Link>
|
||||||
</Suspense>
|
</Container>
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
/* import chaptersStatic from './chapters.json' assert { type: 'json' }; */
|
import { setTimeout } from 'timers/promises';
|
||||||
|
|
||||||
import Head from 'next/head'
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import chaptersStatic from './chapters.json' assert { type: 'json' };
|
||||||
|
|
||||||
import { Container } from '@/components/Container'
|
import { Container } from '@/components/Container'
|
||||||
import { FormattedDate } from '@/components/FormattedDate'
|
import { FormattedDate } from '@/components/FormattedDate'
|
||||||
@@ -25,9 +27,41 @@ function humanTime(time) {
|
|||||||
return stepOne.length > 0 ? stepOne[0] : '';
|
return stepOne.length > 0 ? stepOne[0] : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Page({ params }) {
|
function Transcript({ children }) {
|
||||||
const episode = await getEpisode({ episodeSlug: params.slug })
|
return (
|
||||||
|
<>
|
||||||
|
<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">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function Skeleton({ width, height, className, color }) {
|
||||||
|
const w = width ? '' : 'w-full';
|
||||||
|
const c = color ? color : 'bg-slate-200';
|
||||||
|
return (
|
||||||
|
<span className={`animate-pulse ${c} ${w} ${className}`} style={{ width, height }} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function TranscriptNoPlayer({ episode }) {
|
||||||
|
return (
|
||||||
|
<Transcript>
|
||||||
|
{episode.transcript.map(({ id, text }) => (
|
||||||
|
<p key={id}>
|
||||||
|
<Skeleton className="inline-flex" width="91px" height="18px" /> {text}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</Transcript>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function TranscriptWithPlayer({ episode }) {
|
||||||
const chaptersRes = episode?.chapters && await fetch(episode.chapters, { cache: 'no-store' });
|
const chaptersRes = episode?.chapters && await fetch(episode.chapters, { cache: 'no-store' });
|
||||||
|
/* await setTimeout(5000); */
|
||||||
/* const { chapters } = chaptersStatic */
|
/* const { chapters } = chaptersStatic */
|
||||||
const { chapters } = chaptersRes ? await chaptersRes.json() : { chapters: null }
|
const { chapters } = chaptersRes ? await chaptersRes.json() : { chapters: null }
|
||||||
let chapterOffsets = [[0, 0]]
|
let chapterOffsets = [[0, 0]]
|
||||||
@@ -41,6 +75,23 @@ export default async function Page({ params }) {
|
|||||||
}, { startTime: 0, title: '', acc: 0 })
|
}, { startTime: 0, title: '', acc: 0 })
|
||||||
}
|
}
|
||||||
chapterOffsets = chapterOffsets.reverse()
|
chapterOffsets = chapterOffsets.reverse()
|
||||||
|
return (
|
||||||
|
<Transcript>
|
||||||
|
{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>
|
||||||
|
))}
|
||||||
|
</Transcript>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Page({ params }) {
|
||||||
|
const episode = await getEpisode({ episodeSlug: params.slug })
|
||||||
let date = new Date(episode.published)
|
let date = new Date(episode.published)
|
||||||
|
|
||||||
let audioPlayerData = {
|
let audioPlayerData = {
|
||||||
@@ -78,23 +129,9 @@ export default async function Page({ params }) {
|
|||||||
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"
|
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' }}
|
dangerouslySetInnerHTML={{ __html: episode.content || 'CONTENT' }}
|
||||||
/>
|
/>
|
||||||
{episode?.transcript && (
|
{episode?.transcript && <Suspense fallback={<TranscriptNoPlayer episode={episode} />}>
|
||||||
<>
|
<TranscriptWithPlayer episode={episode} />
|
||||||
<hr className="my-12 border-gray-200" />
|
</Suspense>}
|
||||||
<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>
|
</Container>
|
||||||
</article>
|
</article>
|
||||||
</>
|
</>
|
||||||
|
|||||||
96
src/components/Episodes.jsx
Normal file
96
src/components/Episodes.jsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Episodes({ limit }) {
|
||||||
|
const episodes = await getEpisodes()
|
||||||
|
|
||||||
|
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>
|
||||||
|
<div className="divide-y divide-slate-100 sm:mt-4 lg:mt-8 lg:border-t lg:border-slate-100">
|
||||||
|
{episodes.slice(0, limit || episodes.length).map((episode) => (
|
||||||
|
<EpisodeEntry key={episode.id} episode={episode} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import path from 'path';
|
|||||||
|
|
||||||
import { extractFromXml } from '@extractus/feed-extractor'
|
import { extractFromXml } from '@extractus/feed-extractor'
|
||||||
|
|
||||||
|
export const PAGE_SIZE = 15;
|
||||||
|
|
||||||
const episodeExtra = {
|
const episodeExtra = {
|
||||||
'Buzzsprout-12158608': {
|
'Buzzsprout-12158608': {
|
||||||
slug: 'how-using-typescript-actually-makes-your-program-worse',
|
slug: 'how-using-typescript-actually-makes-your-program-worse',
|
||||||
|
|||||||
Reference in New Issue
Block a user