From 32cfcf8d0709d382c22254f75af7a68a98b5aabf Mon Sep 17 00:00:00 2001 From: Santosh Yadav Date: Thu, 16 Jul 2026 22:24:43 +0200 Subject: [PATCH 1/2] fix: show all recent stories by merging index with legacy KV scan When the recent stories index exists but has fewer entries than the requested limit (e.g. only stories created after the index feature was deployed), the handler now falls through to the legacy KV scan to pick up older pre-index stories. Duplicate entries are skipped by tracking indexed keys in a Set. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- yourstory/src/server.ts | 53 +++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/yourstory/src/server.ts b/yourstory/src/server.ts index efbde06..92f51e9 100644 --- a/yourstory/src/server.ts +++ b/yourstory/src/server.ts @@ -945,27 +945,47 @@ async function handleRecentStories( const indexRaw = await env.STORY_KV.get(RECENT_STORIES_INDEX_KEY); const origin = url.origin; + // Start with indexed entries if available + const indexedStories: Array<{ + username: string; + genre: string; + title: string; + story: string; + updatedAt?: string; + shareUrl: string; + imageUrl?: string; + }> = []; + // Track indexed story keys to avoid duplicates when merging with legacy scan + const indexedKeys = new Set(); + if (indexRaw) { const index: RecentStoryEntry[] = JSON.parse(indexRaw); - const stories = index.slice(0, limit).map((entry) => ({ - username: entry.username, - genre: entry.genre, - title: entry.title, - story: entry.story, - updatedAt: entry.updatedAt, - shareUrl: `${origin}/story/${encodeURIComponent(entry.handle)}/${encodeURIComponent(entry.genre)}`, - ...(entry.hasImage ? { imageUrl: `${origin}/api/stories/image/${encodeURIComponent(entry.handle)}/${encodeURIComponent(entry.genre)}` } : {}), - })); - return json({ stories }); + for (const entry of index.slice(0, limit)) { + indexedStories.push({ + username: entry.username, + genre: entry.genre, + title: entry.title, + story: entry.story, + updatedAt: entry.updatedAt, + shareUrl: `${origin}/story/${encodeURIComponent(entry.handle)}/${encodeURIComponent(entry.genre)}`, + ...(entry.hasImage ? { imageUrl: `${origin}/api/stories/image/${encodeURIComponent(entry.handle)}/${encodeURIComponent(entry.genre)}` } : {}), + }); + indexedKeys.add(`${entry.handle}:${entry.genre}`); + } + + // If the index already has enough entries, return early + if (indexedStories.length >= limit) { + return json({ stories: indexedStories }); + } } - // Legacy fallback: scan KV keys for namespaces without an index (pre-deployment stories) + // Legacy fallback: scan KV keys to fill remaining slots (pre-deployment stories) const listed = await env.STORY_KV.list(); const storyKeys = listed.keys.filter( (k) => !k.name.endsWith(':imageKey') && k.name !== RECENT_STORIES_INDEX_KEY, ); - const stories: Array<{ + const legacyStories: Array<{ username: string; genre: string; title: string; @@ -976,6 +996,7 @@ async function handleRecentStories( }> = []; for (const key of storyKeys) { + if (indexedKeys.has(key.name)) continue; try { const raw = await env.STORY_KV.get(key.name); if (!raw) continue; @@ -987,7 +1008,7 @@ async function handleRecentStories( if (!handle || !genre) continue; const imageKey = parsed.imageKey ?? (await env.STORY_KV.get(`${key.name}:imageKey`)) ?? undefined; - stories.push({ + legacyStories.push({ username: parsed.username || handle, genre: parsed.genre || genre, title: parsed.title, @@ -1001,13 +1022,15 @@ async function handleRecentStories( } } - stories.sort((a, b) => { + legacyStories.sort((a, b) => { const dateA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0; const dateB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0; return dateB - dateA; }); - return json({ stories: stories.slice(0, limit) }); + // Merge: indexed entries first (already sorted), then legacy entries + const allStories = [...indexedStories, ...legacyStories]; + return json({ stories: allStories.slice(0, limit) }); } catch (err) { console.error('Recent stories error:', err); return json({ error: 'Failed to fetch recent stories' }, 500); From 8e4e76f9c2dc3fc5eab12087e16acc6ad15f7394 Mon Sep 17 00:00:00 2001 From: Santosh Yadav Date: Thu, 16 Jul 2026 22:26:24 +0200 Subject: [PATCH 2/2] feat: load 10 recent stories with Load More pagination - Server: add offset query param, return hasMore flag, increase default limit from 5 to 10 (max 50) - Service: pass offset param to API - Dashboard: load 10 stories initially, show 'Load More' button when more are available, append next page on click with loading spinner Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src/app/core/services/story.service.ts | 5 ++- .../pages/dashboard/dashboard.component.ts | 40 ++++++++++++++++++- yourstory/src/server.ts | 14 ++++--- 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/yourstory/src/app/core/services/story.service.ts b/yourstory/src/app/core/services/story.service.ts index c5f612f..f577821 100644 --- a/yourstory/src/app/core/services/story.service.ts +++ b/yourstory/src/app/core/services/story.service.ts @@ -37,6 +37,7 @@ export interface RecentStory { export interface RecentStoriesResponse { stories: RecentStory[]; + hasMore?: boolean; } export interface ImageGenerationRequest { @@ -86,8 +87,8 @@ export class StoryService { /** * Fetches the most recently generated stories. */ - getRecentStories(limit = 5): Observable { - return this.http.get(`/api/stories/recent?limit=${limit}`); + getRecentStories(limit = 10, offset = 0): Observable { + return this.http.get(`/api/stories/recent?limit=${limit}&offset=${offset}`); } /** diff --git a/yourstory/src/app/pages/dashboard/dashboard.component.ts b/yourstory/src/app/pages/dashboard/dashboard.component.ts index fd837f1..28283fa 100644 --- a/yourstory/src/app/pages/dashboard/dashboard.component.ts +++ b/yourstory/src/app/pages/dashboard/dashboard.component.ts @@ -303,6 +303,27 @@ import { SharePlatform } from '../../core/constants/share.constants'; } + @if (hasMoreStories()) { +
+ +
+ } } } @@ -890,7 +911,10 @@ export class DashboardComponent { readonly shareNotification = signal(null); readonly recentStories = signal([]); readonly isLoadingRecentStories = signal(false); + readonly isLoadingMoreStories = signal(false); + readonly hasMoreStories = signal(false); private recentStoriesRequestId = 0; + private readonly STORIES_PAGE_SIZE = 10; readonly totalInsightContributions = computed(() => (this.insightsData() ?? []).reduce((sum, repo) => sum + repo.totalContributions, 0) @@ -943,7 +967,8 @@ export class DashboardComponent { loadRecentStories(): void { const requestId = ++this.recentStoriesRequestId; this.isLoadingRecentStories.set(true); - this.storyService.getRecentStories(5).pipe( + this.hasMoreStories.set(false); + this.storyService.getRecentStories(this.STORIES_PAGE_SIZE).pipe( catchError(() => EMPTY), finalize(() => { if (requestId === this.recentStoriesRequestId) { @@ -953,10 +978,23 @@ export class DashboardComponent { ).subscribe((res) => { if (requestId === this.recentStoriesRequestId) { this.recentStories.set(res.stories); + this.hasMoreStories.set(res.hasMore ?? false); } }); } + loadMoreStories(): void { + const currentStories = this.recentStories(); + this.isLoadingMoreStories.set(true); + this.storyService.getRecentStories(this.STORIES_PAGE_SIZE, currentStories.length).pipe( + catchError(() => EMPTY), + finalize(() => this.isLoadingMoreStories.set(false)), + ).subscribe((res) => { + this.recentStories.set([...currentStories, ...res.stories]); + this.hasMoreStories.set(res.hasMore ?? false); + }); + } + onSearchUser(afterLoad?: () => void): void { const username = this.usernameInput().trim(); if (!username) return; diff --git a/yourstory/src/server.ts b/yourstory/src/server.ts index 92f51e9..01087fe 100644 --- a/yourstory/src/server.ts +++ b/yourstory/src/server.ts @@ -937,8 +937,10 @@ async function handleRecentStories( env: Env, ): Promise { const url = new URL(request.url); - const requestedLimit = Number.parseInt(url.searchParams.get('limit') ?? '5', 10); - const limit = Number.isNaN(requestedLimit) ? 5 : Math.min(Math.max(requestedLimit, 1), 20); + const requestedLimit = Number.parseInt(url.searchParams.get('limit') ?? '10', 10); + const limit = Number.isNaN(requestedLimit) ? 10 : Math.min(Math.max(requestedLimit, 1), 50); + const requestedOffset = Number.parseInt(url.searchParams.get('offset') ?? '0', 10); + const offset = Number.isNaN(requestedOffset) ? 0 : Math.max(requestedOffset, 0); try { // Read the pre-built recent stories index @@ -974,8 +976,9 @@ async function handleRecentStories( } // If the index already has enough entries, return early - if (indexedStories.length >= limit) { - return json({ stories: indexedStories }); + if (indexedStories.length >= offset + limit) { + const page = indexedStories.slice(offset, offset + limit); + return json({ stories: page, hasMore: indexedStories.length > offset + limit }); } } @@ -1030,7 +1033,8 @@ async function handleRecentStories( // Merge: indexed entries first (already sorted), then legacy entries const allStories = [...indexedStories, ...legacyStories]; - return json({ stories: allStories.slice(0, limit) }); + const page = allStories.slice(offset, offset + limit); + return json({ stories: page, hasMore: allStories.length > offset + limit }); } catch (err) { console.error('Recent stories error:', err); return json({ error: 'Failed to fetch recent stories' }, 500);