Adds basic fetch

- 🐛 cors
- 🐛 `.env.js`/`ENV`
This commit is contained in:
2026-06-11 16:35:07 +02:00
parent 97983aff87
commit bcb51d5397
9 changed files with 148 additions and 10 deletions
+2
View File
@@ -3,3 +3,5 @@
- [x] initial setup (`bs`, app structure, …) - [x] initial setup (`bs`, app structure, …)
- [x] adds routing support - [x] adds routing support
- [ ] [episodes basic fetch](./plans/plan-basic-episodes.md) - [ ] [episodes basic fetch](./plans/plan-basic-episodes.md)
- [ ] :bug: cors
- [ ] :bug: `.env.js`/`ENV`
+1 -1
View File
@@ -17,7 +17,7 @@ export interface FeedEntriesResponse {
* @param feedId - The ID of the feed. * @param feedId - The ID of the feed.
*/ */
export async function getEpisodes(feedId: string= "all"): Promise<Episode[]> { export async function getEpisodes(feedId: string= "all"): Promise<Episode[]> {
const response = await fetchAPI(`feed/entries?feed_id=${feedId}`); const response = await fetchAPI(`category/entries?id=${feedId}`);
if (!response.ok) if (!response.ok)
throw new Error(`Failed to fetch episodes: ${response.statusText}`); throw new Error(`Failed to fetch episodes: ${response.statusText}`);
const data = await response.json() as FeedEntriesResponse; const data = await response.json() as FeedEntriesResponse;
+9 -5
View File
@@ -4,8 +4,6 @@ const url_base = `${url_server}/rest`;
import { users } from "ENV"; import { users } from "ENV";
const [ username, password ] = users[0].split(':'); const [ username, password ] = users[0].split(':');
console.log(import.meta);
/** /**
* Generates the Basic Authentication header. * Generates the Basic Authentication header.
*/ */
@@ -24,11 +22,17 @@ export async function fetchAPI(endpoint: string, options: RequestInit = {}): Pro
const url = `${url_base}/${endpoint}`; const url = `${url_base}/${endpoint}`;
const auth = authHeader(); const auth = authHeader();
const headers = new Headers(options.headers); options = { ...options };
headers.set('Authorization', auth); const { headers = {} } = options;
Reflect.deleteProperty(options, "headers");
return fetch(url, { return fetch(url, {
...options, ...options,
headers, headers: {
Authorization: auth,
Accept: "application/json",
...headers,
},
mode: "no-cors",
}); });
} }
+11
View File
@@ -6,4 +6,15 @@ export const styles = css`
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
} }
.episode-list {
padding: 0;
margin: 0;
width: 100%;
max-width: 600px;
}
.error {
color: red;
}
`; `;
+43 -3
View File
@@ -1,14 +1,54 @@
import { LitElement, html } from "lit"; import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js"; import { customElement, state, property } from "lit/decorators.js";
import { styles } from "./index.css.js"; import { styles } from "./index.css.js";
import { getEpisodes, type Episode } from "../api/episodes.js";
import "../components/c-episode-list-card/index.js";
@customElement("app-episodes") @customElement("app-episodes")
export class AppEpisodes extends LitElement { export class AppEpisodes extends LitElement {
static override styles = styles; static override styles = styles;
@property({ type: String }) feedId: string = "all";
@state() private episodes: Episode[] = [];
@state() private loading = true;
@state() private error: string | null = null;
override async firstUpdated() {
await this.fetchEpisodes();
}
private async fetchEpisodes() {
this.loading = true;
this.error = null;
try {
this.episodes = await getEpisodes(this.feedId);
} catch (e) {
this.error = e instanceof Error ? e.message : "An unknown error occurred";
} finally {
this.loading = false;
}
}
override render() { override render() {
if (this.loading) {
return html`<p>Loading episodes...</p>`;
}
if (this.error) {
return html`<p class="error">Error: ${this.error}</p>`;
}
if (this.episodes.length === 0) {
return html`<p>No episodes found.</p>`;
}
return html` return html`
<div class="logo"></div> <div class="logo">
<a href="/episodes/1">Episode 1</a> <h1>Episodes</h1>
</div>
<ul class="episode-list">
${this.episodes.map((episode) => html`<c-episode-list-card .episode=${episode}></c-episode-list-card>`)}
</ul>
`; `;
} }
} }
+1 -1
View File
@@ -9,7 +9,7 @@ export class AppHome extends LitElement {
return html` return html`
<h1>My app</h1> <h1>My app</h1>
<p>Hello world</p> <p>Hello world</p>
<a href="/episodes">Episode</a> <a href="/episodes">Episodes</a>
`; `;
} }
} }
@@ -0,0 +1,28 @@
import { css } from "lit";
export const styles = css`
:host {
list-style: none;
padding: 1rem;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.info {
display: flex;
flex-direction: column;
}
.title {
font-weight: bold;
text-decoration: none;
color: var(--primary-color, #007bff);
}
time {
font-size: 0.85rem;
color: #666;
}
`;
@@ -0,0 +1,30 @@
import { html } from "lit";
import { fixture, expect } from "@open-wc/testing";
import { type Episode } from "@/api/episodes.js";
import "./index.js";
describe("EpisodeListItem", () => {
const mockEpisode: Episode = {
id: "1",
title: "Test Episode",
published: "2023-01-01T00:00:00Z",
audio_url: "http://example.com/audio.mp3",
};
it("renders correctly with episode data", async () => {
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");
expect(title?.textContent).to.equal("Test Episode");
expect(time?.textContent).to.contain("2023");
expect(audioLink).to.exist;
expect(audioLink?.getAttribute("href")).to.equal("http://example.com/audio.mp3");
});
it("renders empty when no episode is provided", async () => {
const el = await fixture(html`<episode-list-card></episode-list-card>`);
expect(el.shadowRoot!.innerHTML.trim()).to.equal("");
});
});
@@ -0,0 +1,23 @@
import { LitElement, html } from "lit";
import { property, customElement } from "lit/decorators.js";
import { type Episode } from "@/api/episodes.js";
import { styles } from "./index.css.js";
@customElement("c-episode-list-card")
export class EpisodeListCard extends LitElement {
static override styles = styles;
@property({ type: Object }) episode!: Episode;
override render() {
if (!this.episode) return html``;
return html`
<a href="/episodes/${this.episode.id}">
<div class="info">
${this.episode.title}
<time datetime="${this.episode.published}">${new Date(this.episode.published).toLocaleDateString()}</time>
</div>
</a>
`;
}
}