import { writable } from 'svelte/store'; import type { Feed, FeedEntry } from './types'; // Create a store for feeds function createFeedsStore() { const { subscribe, set, update } = writable([]); // Create a store object with methods const store = { subscribe, load: async (): Promise => { try { const response = await fetch('/api/feeds'); if (!response.ok) { throw new Error(`Failed to load feeds: ${response.statusText}`); } const feeds = await response.json(); // Convert date strings to Date objects const processedFeeds = feeds.map( ( feed: Omit & { lastFetched?: string; createdAt: string; updatedAt: string; } ) => ({ ...feed, lastFetched: feed.lastFetched ? new Date(feed.lastFetched) : undefined, createdAt: new Date(feed.createdAt), updatedAt: new Date(feed.updatedAt) }) ); set(processedFeeds); return processedFeeds; } catch (error) { console.error('Error loading feeds:', error); throw error; } }, add: async (url: string): Promise => { try { const response = await fetch('/api/feeds', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }) }); if (!response.ok) { throw new Error(`Failed to add feed: ${response.statusText}`); } const newFeed = await response.json(); // Convert date strings to Date objects const processedFeed = { ...newFeed, lastFetched: newFeed.lastFetched ? new Date(newFeed.lastFetched) : undefined, createdAt: new Date(newFeed.createdAt), updatedAt: new Date(newFeed.updatedAt) }; update((feeds) => [...feeds, processedFeed]); return processedFeed; } catch (error) { console.error('Error adding feed:', error); throw error; } }, delete: async (id: string): Promise => { try { const response = await fetch(`/api/feeds/${id}`, { method: 'DELETE' }); if (!response.ok) { throw new Error(`Failed to delete feed: ${response.statusText}`); } update((feeds) => feeds.filter((feed) => feed.id !== id)); } catch (error) { console.error('Error deleting feed:', error); throw error; } }, refresh: async (id?: string): Promise => { try { const endpoint = id ? `/api/feeds/${id}/refresh` : '/api/feeds/refresh'; const response = await fetch(endpoint, { method: 'POST' }); if (!response.ok) { throw new Error(`Failed to refresh feeds: ${response.statusText}`); } // Reload feeds to get updated data await store.load(); } catch (error) { console.error('Error refreshing feeds:', error); throw error; } }, importOPML: async (file: File): Promise<{ imported: number }> => { try { // Validate file type if ( !file.name.toLowerCase().endsWith('.opml') && !file.name.toLowerCase().endsWith('.xml') ) { throw new Error('Please select a valid OPML or XML file'); } // Validate file size (max 10MB) const maxSize = 10 * 1024 * 1024; // 10MB if (file.size > maxSize) { throw new Error('File size exceeds the maximum limit of 10MB'); } console.log('Importing OPML file:', file.name, 'Size:', file.size, 'Type:', file.type); const formData = new FormData(); formData.append('file', file); try { const response = await fetch('/api/feeds/import', { method: 'POST', body: formData }); console.log('OPML import response status:', response.status, response.statusText); // Log headers in a type-safe way const headerLog: Record = {}; response.headers.forEach((value, key) => { headerLog[key] = value; }); console.log('OPML import response headers:', headerLog); // Handle non-OK responses if (!response.ok) { const contentType = response.headers.get('content-type'); console.log('OPML import error content-type:', contentType); if (contentType && contentType.includes('application/json')) { // Try to parse error as JSON try { const errorData = await response.json(); console.log('OPML import error data:', errorData); throw new Error(errorData.error || `Failed to import OPML: ${response.statusText}`); } catch (jsonError) { console.error('Error parsing JSON error response:', jsonError); throw new Error( `Failed to import OPML: ${response.statusText} (Error parsing error response)` ); } } else { // Handle non-JSON responses try { const text = await response.text(); console.log('OPML import error text (first 200 chars):', text.substring(0, 200)); throw new Error( `Server returned HTML instead of JSON. Status: ${response.status} ${response.statusText}` ); } catch (textError) { console.error('Error reading response text:', textError); throw new Error( `Failed to import OPML: ${response.statusText} (Could not read response)` ); } } } const result = await response.json(); console.log('OPML import success:', result); // Reload feeds to get the newly imported ones await store.load(); return result; } catch (fetchError) { console.error('OPML import fetch error:', fetchError); throw fetchError; } } catch (error) { console.error('Error importing OPML:', error); throw error; } }, importOPMLFromURL: async (url: string): Promise<{ imported: number }> => { try { const response = await fetch('/api/feeds/import-url', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }) }); if (!response.ok) { throw new Error(`Failed to import OPML from URL: ${response.statusText}`); } const result = await response.json(); // Reload feeds to get the newly imported ones await store.load(); return result; } catch (error) { console.error('Error importing OPML from URL:', error); throw error; } } }; return store; } // Create a store for feed entries function createEntriesStore() { const { subscribe, set, update } = writable([]); return { subscribe, loadEntries: async (feedId?: string, unreadOnly = false): Promise => { try { const params = new URLSearchParams(); if (feedId) params.append('feedId', feedId); if (unreadOnly) params.append('unreadOnly', 'true'); const response = await fetch(`/api/feeds/entries?${params.toString()}`); if (!response.ok) throw new Error('Failed to load entries'); const data = await response.json(); const entries = data.map((entry: FeedEntry) => ({ ...entry, published: entry.published ? new Date(entry.published) : null, updated: entry.updated ? new Date(entry.updated) : null, readAt: entry.readAt ? new Date(entry.readAt) : null })); set(entries); return entries; } catch (error) { console.error('Error loading entries:', error); throw error; } }, getEntry: async (id: string): Promise => { try { const response = await fetch(`/api/feeds/entries/${id}`); if (!response.ok) throw new Error('Failed to load entry'); const entry = await response.json(); return { ...entry, published: entry.published ? new Date(entry.published) : null, updated: entry.updated ? new Date(entry.updated) : null, readAt: entry.readAt ? new Date(entry.readAt) : null }; } catch (error) { console.error('Error loading entry:', error); throw error; } }, markAsRead: async (id: string): Promise => { try { const response = await fetch(`/api/feeds/entries/${id}/read`, { method: 'POST' }); if (!response.ok) throw new Error('Failed to mark entry as read'); // Update the entry in the store update((entries) => { return entries.map((entry) => { if (entry.id === id) { return { ...entry, readAt: new Date() }; } return entry; }); }); } catch (error) { console.error('Error marking entry as read:', error); throw error; } }, markAllAsRead: async (feedId?: string): Promise => { try { const params = new URLSearchParams(); if (feedId) params.append('feedId', feedId); const response = await fetch(`/api/feeds/entries/read-all?${params.toString()}`, { method: 'POST' }); if (!response.ok) throw new Error('Failed to mark all entries as read'); // Update all entries in the store update((entries) => { return entries.map((entry) => { if (!entry.readAt && (!feedId || entry.feedId === feedId)) { return { ...entry, readAt: new Date() }; } return entry; }); }); } catch (error) { console.error('Error marking all entries as read:', error); throw error; } }, fetchFullContent: async (id: string): Promise => { try { const response = await fetch(`/api/feeds/entries/${id}/full-content`, { method: 'POST' }); if (!response.ok) throw new Error('Failed to fetch full content'); } catch (error) { console.error('Error fetching full content:', error); throw error; } } }; } export const feeds = createFeedsStore(); export const entries = createEntriesStore();