⚡ Adds basic fetch
- 🐛 cors - 🐛 `.env.js`/`ENV`
This commit is contained in:
@@ -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
@@ -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
@@ -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",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user