quicknotes/frontend/src/lib/feeds.ts

327 lines
9.3 KiB
TypeScript
Raw Normal View History

import { writable } from 'svelte/store';
import type { Feed, FeedEntry } from './types';
// Create a store for feeds
function createFeedsStore() {
const { subscribe, set, update } = writable<Feed[]>([]);
// Create a store object with methods
const store = {
subscribe,
load: async (): Promise<Feed[]> => {
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<Feed, 'lastFetched' | 'createdAt' | 'updatedAt'> & {
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<Feed> => {
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<void> => {
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<void> => {
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<string, string> = {};
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<FeedEntry[]>([]);
return {
subscribe,
loadEntries: async (feedId?: string, unreadOnly = false): Promise<FeedEntry[]> => {
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<FeedEntry> => {
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<void> => {
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<void> => {
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<void> => {
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();