feat(frontend): Vue 3 wizard UI (nostr login, episode upload, go-live)

- NIP-07 login → NIP-98 signed request → session cookie
- episode wizard: pick/create podcast (incl. lightning address question),
  browser-side sha256 + BUD-02 blossom upload with progress, feed result page
- live wizard + stream dashboard: OBS server/key (shown once, rotatable),
  browser WHIP publishing (camera or screen), hls.js live preview,
  publish-recording-as-episode flow
- settings view: external blossom URL, relay list, public URL (admin only)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 19:06:43 +00:00
co-authored by Claude Fable 5
parent 594a9a8783
commit 5216c6451e
26 changed files with 4144 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
// Thin fetch wrapper for the podpuddle API (session cookie auth).
export class ApiError extends Error {
constructor(public status: number, message: string) {
super(message);
}
}
async function request<T>(method: string, path: string, body?: unknown, headers?: Record<string, string>): Promise<T> {
const res = await fetch(path, {
method,
credentials: 'same-origin',
headers: {
...(body !== undefined ? { 'content-type': 'application/json' } : {}),
...headers,
},
body: body !== undefined ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
let message = res.statusText;
try {
message = ((await res.json()) as { error?: string }).error ?? message;
} catch {
/* keep statusText */
}
throw new ApiError(res.status, message);
}
return (await res.json()) as T;
}
export const api = {
get: <T>(path: string) => request<T>('GET', path),
post: <T>(path: string, body?: unknown, headers?: Record<string, string>) =>
request<T>('POST', path, body, headers),
put: <T>(path: string, body?: unknown) => request<T>('PUT', path, body),
delete: <T>(path: string) => request<T>('DELETE', path),
};