Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ import { blogPostRoute } from "./routes/blog-post-route.tsx";
import { blogIndexRoute } from "./routes/blog-index-route.tsx";
import { blogTagRoute } from "./routes/blog-tag-route.tsx";
import { initBlog } from "./blog/blog.ts";
import { podcastIndexRoute } from "./routes/podcast-index-route.tsx";
import { initSimpleCast } from "./podcast/podcast.ts";
import { podcastRoute } from "./routes/podcast-route.tsx";

await main(function* () {
let proxies = proxySites();

yield* initBlog();
yield* initSimpleCast(Deno.env.get("SIMPLECAST_API_KEY"));

let revolution = createRevolution({
app: [
Expand All @@ -32,6 +36,8 @@ await main(function* () {
route("/blog/:id", blogPostRoute()),
route("/blog/tags/:tag", blogTagRoute()),
route("/blog(.*)", assetsRoute("blog")),
route("/podcast", podcastIndexRoute()),
route("/podcast/:id", podcastRoute()),
route("/backstage", backstageServicesRoute()),
route("/dx-consulting", dxConsultingServicesRoute()),
route("/work/case-studies/resideo", resideoBackstageCaseStudyRoute()),
Expand Down
218 changes: 218 additions & 0 deletions podcast/podcast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { all, call, createContext, Operation, useAbortSignal } from "effection";

const PodcastContext = createContext<Episode[]>(
"podcast",
);

export interface SimplecastClient {
getEpisodes(name: string): Operation<Episode[]>;
}

export interface Podcast {
readonly title: string;
readonly id: string;
}

export interface Episode {
linkname: string;
season: {
href: string;
number: number;
next_episode_number: number;
};
audio_file_name: string;
is_explicit: boolean;
waveform_pack: string;
audio_file_url: string;
sponsors: {
href: string;
};
number: number;
authors: {
href: string;
collection: Array<
{
href: string;
name: string;
id: string;
}
>;
};
analytics: {
href: string;
};
long_description: string;
podcast: {
id: string;
href: string;
title: string;
status: "published";
image_url: string;
episodes: { count: number };
created_at: string;
account_id: string;
account: {
id: string;
href: string;
owner: {
name: string;
id: string;
email: string;
};
};
};
description: string;
audio_status: "transcoded";
legacy_id: number;
transcription: string | null;
audio_file_size: number;
waveform_json: string;
slug: string;
title: string;
campaign_preview: {
href: string;
};
is_hidden: false;
is_published: true;
warnings: Record<string | number | symbol, string>;
audio_file_path: string;
dashboard_link: string;
audio_content_type: string;
episode_feeds: [
{
id: string;
feed_id: string;
},
];
days_since_release: number;
published_at: string;
href: string;
audio: {
href: string;
};
image_url: string;
id: string;
enclosure_url: string;
ad_free_audio_file_url: string;
duration: number;
keywords: {
href: string;
collection: Array<
{
href: string;
value: string;
id: string;
hide: false;
}
>;
};
token: string;
guid: string;
created_at: string;
image_path: string;
episode_url: string;
audio_file_path_tc: string;
updated_at: string;
audio_file: {
url: string;
size: number;
path_tc: string;
path: string;
name: string;
href: string;
headliner_url: string;
ad_free_url: string;
};
}

export function* initSimpleCast(apiKey?: string) {
if (!apiKey) {
console.log(`simplecast: disabled`);
yield* PodcastContext.set([]);
} else {
let client = new HTTPClient({ apiKey });
let episodes = yield* client.getEpisodes("The Frontside Podcast");
console.dir(episodes[0].linkname);
console.log(`simplecast: loaded ${episodes.length} episodes`);
yield* PodcastContext.set(episodes);
}
}

export function* usePodcastEpisodes(): Operation<Episode[]> {
return yield* PodcastContext;
}

interface HTTPCLientOptions {
apiKey: string;
}

class HTTPClient implements SimplecastClient {
constructor(public readonly options: HTTPCLientOptions) {}

*getEpisodes(title: string): Operation<Episode[]> {
let podcasts = yield* this.getPodcasts();
let podcast = podcasts.find((p) => p.title === title);
if (!podcast) {
throw new Error(
`unable to find podcast: ${title} in [${
podcasts.map((p) => p.title).join(", ")
}]`,
);
}

let response = yield* this.request(
`/podcasts/${podcast.id}/episodes`,
{ limit: 1000, offset: 0 },
);

let json = yield* call(() => response.json());
return (yield* all(
json.collection.map((episodeMetadata: { id: string }) => {
let request = this.request.bind(this);
return call(function* () {
let response = yield* request(`/episodes/${episodeMetadata.id}`);
let episode = yield* call(() => response.json());
return {
...episode,
linkname: episode.title.toLowerCase().replaceAll(/\s/g, "-"),
};
});
}),
)) as Episode[];
}

*getPodcasts(): Operation<Podcast[]> {
let response = yield* this.request("/podcasts");
let json = yield* call(() => response.json());
return json.collection;
}

private *request(
pathname: string,
params: Record<string, string | number> = {},
): Operation<Response> {
let url = new URL(`https://api.simplecast.com`);
url.pathname = pathname;
let searchParams: Record<string, string> = {};
for (let key in params) {
searchParams[key] = String(params[key]);
}
url.search = new URLSearchParams(searchParams).toString();
let signal = yield* useAbortSignal();
let response = yield* call(() =>
fetch(url, {
signal,
headers: {
"Authorization": `Bearer ${this.options.apiKey}`,
},
})
);
if (!response.ok) {
throw new Error(`${response.status}: ${response.statusText}`, {
cause: pathname,
});
} else {
return response;
}
}
}
39 changes: 39 additions & 0 deletions routes/podcast-index-route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { type JSXHandler } from "revolution/jsx-runtime";
import { usePodcastEpisodes } from "../podcast/podcast.ts";

import { useAppHtml } from "./app.html.tsx";

export function podcastIndexRoute(): JSXHandler {
return function* () {
let AppHtml = yield* useAppHtml({
title:
"The Frontside Podcast | Engineering, Developer Experience, Testing and Tech Leadership",
description:
"The Frontside Podcast dive into engineering, developer experience, and tech leadership. Join industry experts as they share insights on modern software development, testing strategies, and more.",
ogImage: "../assets/img/frontside-logo.png",
twitterXImage: "../assets/img/frontside-logo.png",
author: "Frontside",
});

let episodes = yield* usePodcastEpisodes();

return (
<AppHtml>
<article class="mx-auto container">
<h1 class="ml-12">Podcast</h1>
<ol class="md:gap-6 lg:gap-11 space-y-10 md:space-y-0 md:grid md:grid-cols-2 lg:grid-cols-3 mx-auto p-4 max-w-7xl">
{episodes.map((episode) => (
<li class="flex flex-col border-[#f0f0f0] bg-[#fcfcfc] md:mt-0 p-2 md:p-4 border rounded-md h-full prose">
<a class="no-underline" href={`podcast/${episode.linkname}`}>
<h2>{episode.title}</h2>
<p>{episode.duration}</p>
<p>{episode.description}</p>
</a>
</li>
))}
</ol>
</article>
</AppHtml>
);
};
}
32 changes: 32 additions & 0 deletions routes/podcast-route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { type JSXHandler } from "revolution/jsx-runtime";
import { usePodcastEpisodes } from "../podcast/podcast.ts";
import { respondNotFound, useParams } from "revolution";

export function podcastRoute(): JSXHandler {
return function* () {
let { id } = yield* useParams<{ id: string }>();
let episodes = yield* usePodcastEpisodes();

let episode = episodes.find((episode) => episode.linkname === id);
if (!episode) {
return yield* respondNotFound();
}

let authors = episode.authors.collection.map(a => a.name).join(", ")

return (
<html>
<body>
<h1>{episode.title}</h1>
<ul>
<li><strong>description</strong>: {episode.description}</li>
<li><strong>image_url</strong>: {episode.image_url}</li>
<li><strong>duration</strong>: {episode.duration}</li>
<li><strong>audio</strong>: {episode.audio_file_url}</li>
<li><strong>authors</strong>: {authors}</li>
</ul>
</body>
</html>
);
};
}
Loading