From bcb51d539721cd468692fbb02e37abc179ec55c6 Mon Sep 17 00:00:00 2001 From: Jan Andrle Date: Thu, 11 Jun 2026 16:35:07 +0200 Subject: [PATCH] :zap: Adds basic fetch - :bug: cors - :bug: `.env.js`/`ENV` --- changelog/v1.0.md | 2 + src/api/episodes.ts | 2 +- src/api/fetchAPI.ts | 14 ++++-- src/app-episodes/index.css.ts | 11 +++++ src/app-episodes/index.ts | 46 +++++++++++++++++-- src/app-home/index.ts | 2 +- .../c-episode-list-card/index.css.ts | 28 +++++++++++ .../c-episode-list-card/index.test.ts | 30 ++++++++++++ src/components/c-episode-list-card/index.ts | 23 ++++++++++ 9 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 src/components/c-episode-list-card/index.css.ts create mode 100644 src/components/c-episode-list-card/index.test.ts create mode 100644 src/components/c-episode-list-card/index.ts diff --git a/changelog/v1.0.md b/changelog/v1.0.md index 28311f3..cce39db 100644 --- a/changelog/v1.0.md +++ b/changelog/v1.0.md @@ -3,3 +3,5 @@ - [x] initial setup (`bs`, app structure, …) - [x] adds routing support - [ ] [episodes basic fetch](./plans/plan-basic-episodes.md) + - [ ] :bug: cors + - [ ] :bug: `.env.js`/`ENV` diff --git a/src/api/episodes.ts b/src/api/episodes.ts index e94f1f4..5f6c950 100644 --- a/src/api/episodes.ts +++ b/src/api/episodes.ts @@ -17,7 +17,7 @@ export interface FeedEntriesResponse { * @param feedId - The ID of the feed. */ export async function getEpisodes(feedId: string= "all"): Promise { - const response = await fetchAPI(`feed/entries?feed_id=${feedId}`); + const response = await fetchAPI(`category/entries?id=${feedId}`); if (!response.ok) throw new Error(`Failed to fetch episodes: ${response.statusText}`); const data = await response.json() as FeedEntriesResponse; diff --git a/src/api/fetchAPI.ts b/src/api/fetchAPI.ts index 02673b4..9d50f9e 100644 --- a/src/api/fetchAPI.ts +++ b/src/api/fetchAPI.ts @@ -4,8 +4,6 @@ const url_base = `${url_server}/rest`; import { users } from "ENV"; const [ username, password ] = users[0].split(':'); -console.log(import.meta); - /** * Generates the Basic Authentication header. */ @@ -24,11 +22,17 @@ export async function fetchAPI(endpoint: string, options: RequestInit = {}): Pro const url = `${url_base}/${endpoint}`; const auth = authHeader(); - const headers = new Headers(options.headers); - headers.set('Authorization', auth); + options = { ...options }; + const { headers = {} } = options; + Reflect.deleteProperty(options, "headers"); return fetch(url, { ...options, - headers, + headers: { + Authorization: auth, + Accept: "application/json", + ...headers, + }, + mode: "no-cors", }); } diff --git a/src/app-episodes/index.css.ts b/src/app-episodes/index.css.ts index 5c1060e..03c6b9b 100644 --- a/src/app-episodes/index.css.ts +++ b/src/app-episodes/index.css.ts @@ -6,4 +6,15 @@ export const styles = css` align-items: center; justify-content: flex-start; } + + .episode-list { + padding: 0; + margin: 0; + width: 100%; + max-width: 600px; + } + + .error { + color: red; + } `; diff --git a/src/app-episodes/index.ts b/src/app-episodes/index.ts index 5e423fe..6219a1a 100644 --- a/src/app-episodes/index.ts +++ b/src/app-episodes/index.ts @@ -1,14 +1,54 @@ 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 { getEpisodes, type Episode } from "../api/episodes.js"; +import "../components/c-episode-list-card/index.js"; @customElement("app-episodes") export class AppEpisodes extends LitElement { 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() { + if (this.loading) { + return html`

Loading episodes...

`; + } + + if (this.error) { + return html`

Error: ${this.error}

`; + } + + if (this.episodes.length === 0) { + return html`

No episodes found.

`; + } + return html` - - Episode 1 + + `; } } diff --git a/src/app-home/index.ts b/src/app-home/index.ts index 71d6819..7346663 100644 --- a/src/app-home/index.ts +++ b/src/app-home/index.ts @@ -9,7 +9,7 @@ export class AppHome extends LitElement { return html`

My app

Hello world

- Episode + Episodes `; } } diff --git a/src/components/c-episode-list-card/index.css.ts b/src/components/c-episode-list-card/index.css.ts new file mode 100644 index 0000000..d27f3b2 --- /dev/null +++ b/src/components/c-episode-list-card/index.css.ts @@ -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; + } +`; diff --git a/src/components/c-episode-list-card/index.test.ts b/src/components/c-episode-list-card/index.test.ts new file mode 100644 index 0000000..665248f --- /dev/null +++ b/src/components/c-episode-list-card/index.test.ts @@ -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``); + 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``); + expect(el.shadowRoot!.innerHTML.trim()).to.equal(""); + }); +}); diff --git a/src/components/c-episode-list-card/index.ts b/src/components/c-episode-list-card/index.ts new file mode 100644 index 0000000..1c8e0ae --- /dev/null +++ b/src/components/c-episode-list-card/index.ts @@ -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` + +
+ ${this.episode.title} + +
+
+ `; + } +}