import { LitElement, html, css } from "lit";
import { customElement, state } from "lit/decorators.js";
import { store, toBlueskyPostUrl, formatTimestamp } from "../store";
import { apiService } from "../services";
import type { FeedItemViewModel } from "../types";
@customElement("app-ranked-feed-page")
export class AppRankedFeedPage extends LitElement {
@state()
private isLoading = false;
static styles = css`
:host { display: block; padding: 1.5rem; }
.panel {
background: var(--panel-bg, #fff);
border-radius: 0.5rem;
box-shadow: var(--panel-shadow, 0 1px 3px rgba(0,0,0,0.1));
padding: 1.5rem;
}
.panel-header {
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-color, #e5e7eb);
}
.eyebrow {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted, #6b7280);
margin-bottom: 0.25rem;
}
.feed-items {
display: flex;
flex-direction: column;
gap: 1rem;
}
.feed-item {
padding: 1rem;
border: 1px solid var(--border-color, #e5e7eb);
border-radius: 0.5rem;
background: var(--item-bg, #fafafa);
}
.feed-item header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.75rem;
}
.author {
font-weight: 500;
color: var(--text-primary, #1f2937);
}
.author a {
color: inherit;
text-decoration: none;
}
.author a:hover {
text-decoration: underline;
}
.handle {
font-size: 0.875rem;
color: var(--text-muted, #6b7280);
margin-left: 0.5rem;
}
.timestamp {
font-size: 0.75rem;
color: var(--text-muted, #6b7280);
}
.post-text {
font-size: 0.9375rem;
line-height: 1.5;
color: var(--text-secondary, #4b5563);
margin-bottom: 0.75rem;
}
.post-text a {
color: var(--accent-bg, #3b82f6);
text-decoration: none;
}
.post-text a:hover {
text-decoration: underline;
}
.meta {
display: flex;
gap: 1rem;
font-size: 0.75rem;
color: var(--text-muted, #6b7280);
}
.empty-state {
text-align: center;
padding: 3rem 1.5rem;
color: var(--text-muted, #6b7280);
}
.loading {
text-align: center;
padding: 2rem;
color: var(--text-muted, #6b7280);
}
`;
connectedCallback() {
super.connectedCallback();
this.unsubscribe = store.subscribe(() => {
this.requestUpdate();
// Reload feed when session becomes authenticated
if (store.isAuthenticated && store.items.length === 0 && !this.isLoading) {
this.loadFeed();
}
});
// Delay initial load to allow session restoration
setTimeout(() => this.loadFeed(), 100);
}
disconnectedCallback() {
super.disconnectedCallback();
this.unsubscribe?.();
}
private unsubscribe?: () => void;
async loadFeed() {
console.log("[RankedFeedPage] loadFeed called, session:", store.isAuthenticated, store.session.did);
if (!store.isAuthenticated || !store.session.did) {
console.log("[RankedFeedPage] Not authenticated, clearing items");
store.setItems([]);
return;
}
this.isLoading = true;
try {
console.log("[RankedFeedPage] Calling getFeedState...");
const state = await apiService.getFeedState(store.session.did, store.selectedGeneratorId);
console.log("[RankedFeedPage] Got state, items count:", state.feedItems?.length);
store.setItems(state.feedItems);
} catch (err) {
console.error("[RankedFeedPage] Failed to load feed:", err);
store.setError(String(err));
} finally {
this.isLoading = false;
}
}
render() {
const items = store.items;
return html`
<div class="panel">
<div class="panel-header">
<div class="eyebrow">Ranked Feed</div>
<h2>${store.selectedGenerator?.name || "Balanced"}</h2>
</div>
${this.isLoading
? html`<div class="loading">Loading...</div>`
: items.length === 0
? html`
<div class="empty-state">
<p>No items to show.</p>
${!store.isAuthenticated
? html`<p>Please sign in on the Session page.</p>`
: html`<p>Import your likes to see ranked content.</p>`}
</div>
`
: html`
<div class="feed-items">
${items.map((item) => this.renderItem(item))}
</div>
`}
</div>
`;
}
private renderItem(item: FeedItemViewModel) {
const profileUrl = item.authorHandle
? `https://bsky.app/profile/${encodeURIComponent(item.authorHandle)}`
: `https://bsky.app/profile/${encodeURIComponent(item.authorDid)}`;
const postUrl = toBlueskyPostUrl(item);
return html`
<div class="feed-item">
<header>
<div class="author">
<a href=${profileUrl} target="_blank" rel="noopener">
${item.authorDisplayName || item.authorHandle || item.authorDid}
</a>
${item.authorHandle
? html`<span class="handle">@${item.authorHandle}</span>`
: null}
</div>
<span class="timestamp">${formatTimestamp(item.createdAt)}</span>
</header>
<div class="post-text">
<a href=${postUrl} target="_blank" rel="noopener">${item.text}</a>
</div>
<div class="meta">
<span>Semantic: ${item.semanticScore.toFixed(3)}</span>
<span>Affinity: ${item.authorAffinity.toFixed(3)}</span>
</div>
</div>
`;
}
}