Episodes (no paging)

This commit is contained in:
2026-06-12 14:55:38 +02:00
parent 5a0a2de0f0
commit ed632ad3fb
6 changed files with 59 additions and 50 deletions
+4 -3
View File
@@ -3,9 +3,10 @@ import { fetchAPI } from "./fetchAPI.js";
export interface Episode { export interface Episode {
id: string; id: string;
title: string; title: string;
published: string; // ISO string or similar content: string; // HTML
audio_url?: string; date: number;
description?: string; feedId: string;
feedName: string;
} }
export interface FeedEntriesResponse { export interface FeedEntriesResponse {
+3 -9
View File
@@ -4,16 +4,10 @@ export const styles = css`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: flex-start; --width: 33ch;
/* internal */
margin-inline: max(7.5ch, calc(50% - var(--width)));
} }
.episode-list {
padding: 0;
margin: 0;
width: 100%;
max-width: 600px;
}
.error { .error {
color: red; color: red;
} }
+2 -6
View File
@@ -43,12 +43,8 @@ export class AppEpisodes extends LitElement {
} }
return html` return html`
<div class="logo"> <h1>Episodes</h1>
<h1>Episodes</h1> ${this.episodes.map((episode) => html`<c-episode-list-card .episode=${episode}></c-episode-list-card>`)}
</div>
<ul class="episode-list">
${this.episodes.map((episode) => html`<c-episode-list-card .episode=${episode}></c-episode-list-card>`)}
</ul>
`; `;
} }
} }
+22 -19
View File
@@ -2,27 +2,30 @@ import { css } from "lit";
export const styles = css` export const styles = css`
:host { :host {
list-style: none; display: grid;
padding: 1rem; grid-template-areas:
border-bottom: 1px solid #eee; "title title"
display: flex; "date feed"
justify-content: space-between; "content content";
align-items: center; grid-template-rows:
fit-content
fit-content
1fr;
} }
h2 {
.info { grid-area: title;
display: flex; text-wrap: balance;
flex-direction: column; text-wrap: pretty;
} }
time, .feed {
.title {
font-weight: bold;
text-decoration: none;
color: var(--primary-color, #007bff);
}
time {
font-size: 0.85rem;
color: #666; color: #666;
} }
time { grid-area: date; }
.feed { grid-area: feed; }
.content {
grid-area: content;
max-height: 3.5lh;
overflow: auto;
font-size: .9em;
}
`; `;
@@ -7,20 +7,23 @@ describe("EpisodeListItem", () => {
const mockEpisode: Episode = { const mockEpisode: Episode = {
id: "1", id: "1",
title: "Test Episode", title: "Test Episode",
published: "2023-01-01T00:00:00Z", date: 1781258812000,
audio_url: "http://example.com/audio.mp3", content: "<b>Test content</b>",
feedId: "2",
feedName: "Test Feed",
}; };
it("renders correctly with episode data", async () => { it("renders correctly with episode data", async () => {
const el = await fixture(html`<episode-list-card .episode=${mockEpisode}></episode-list-card>`); const el = await fixture(html`<episode-list-card .episode=${mockEpisode}></episode-list-card>`);
const title = el.shadowRoot!.querySelector(".title");
const time = el.shadowRoot!.querySelector("time");
const audioLink = el.shadowRoot!.querySelector(".audio-link");
const title = el.shadowRoot!.querySelector("h2");
expect(title?.textContent).to.equal("Test Episode"); expect(title?.textContent).to.equal("Test Episode");
const time = el.shadowRoot!.querySelector("time");
expect(time?.textContent).to.contain("2023"); expect(time?.textContent).to.contain("2023");
expect(audioLink).to.exist; const feed = el.shadowRoot!.querySelector(".feed");
expect(audioLink?.getAttribute("href")).to.equal("http://example.com/audio.mp3"); expect(feed?.textContent).to.equal("Test Feed");
const content = el.shadowRoot!.querySelector(".content");
expect(content?.innerHTML.trim()).to.equal("<b>Test content</b>");
}); });
it("renders empty when no episode is provided", async () => { it("renders empty when no episode is provided", async () => {
+18 -6
View File
@@ -2,6 +2,7 @@ import { LitElement, html } from "lit";
import { property, customElement } from "lit/decorators.js"; import { property, customElement } from "lit/decorators.js";
import { type Episode } from "@/api/episodes.js"; import { type Episode } from "@/api/episodes.js";
import { styles } from "./index.css.js"; import { styles } from "./index.css.js";
import { templateContent } from "lit/directives/template-content.js";
@customElement("c-episode-list-card") @customElement("c-episode-list-card")
export class EpisodeListCard extends LitElement { export class EpisodeListCard extends LitElement {
@@ -11,13 +12,24 @@ export class EpisodeListCard extends LitElement {
override render() { override render() {
if (!this.episode) return html``; if (!this.episode) return html``;
const { id, title, content, date, feedId, feedName }= this.episode;
const dateString = new Date(date).toLocaleDateString();
const template = document.createElement("template");
try {
// @ts-expect-error 2551
template.setHTML(content);
} catch (e) {
template.innerText = content;
}
return html` return html`
<a href="/episodes/${this.episode.id}"> <h2>
<div class="info"> <a href="/episodes/${id}">${title}</a>
${this.episode.title} </h2>
<time datetime="${this.episode.published}">${new Date(this.episode.published).toLocaleDateString()}</time> <time datetime="${date}">${dateString}</time>
</div> <a class="feed" href="/feeds/${feedId}">${feedName}</a>
</a> <div class="content">
${templateContent(template)}
</div>
`; `;
} }
} }