⚡ Episodes (no paging)
This commit is contained in:
+4
-3
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,12 +43,8 @@ export class AppEpisodes extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="logo">
|
|
||||||
<h1>Episodes</h1>
|
<h1>Episodes</h1>
|
||||||
</div>
|
|
||||||
<ul class="episode-list">
|
|
||||||
${this.episodes.map((episode) => html`<c-episode-list-card .episode=${episode}></c-episode-list-card>`)}
|
${this.episodes.map((episode) => html`<c-episode-list-card .episode=${episode}></c-episode-list-card>`)}
|
||||||
</ul>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
<a class="feed" href="/feeds/${feedId}">${feedName}</a>
|
||||||
|
<div class="content">
|
||||||
|
${templateContent(template)}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user