⚡ Adds basic fetch
- 🐛 cors - 🐛 `.env.js`/`ENV`
This commit is contained in:
@@ -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`
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ export interface FeedEntriesResponse {
|
||||
* @param feedId - The ID of the feed.
|
||||
*/
|
||||
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)
|
||||
throw new Error(`Failed to fetch episodes: ${response.statusText}`);
|
||||
const data = await response.json() as FeedEntriesResponse;
|
||||
|
||||
+9
-5
@@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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`<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`
|
||||
<div class="logo"></div>
|
||||
<a href="/episodes/1">Episode 1</a>
|
||||
<div class="logo">
|
||||
<h1>Episodes</h1>
|
||||
</div>
|
||||
<ul class="episode-list">
|
||||
${this.episodes.map((episode) => html`<c-episode-list-card .episode=${episode}></c-episode-list-card>`)}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export class AppHome extends LitElement {
|
||||
return html`
|
||||
<h1>My app</h1>
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user