Story Collections Feature

This commit is contained in:
Stefan Hardegger
2025-07-25 14:15:23 +02:00
parent 9dd8855914
commit 312093ae2e
42 changed files with 5398 additions and 45 deletions

View File

@@ -1,5 +1,5 @@
import axios from 'axios';
import { AuthResponse, Story, Author, Tag, Series, SearchResult, PagedResult } from '../types/api';
import { AuthResponse, Story, Author, Tag, Series, SearchResult, PagedResult, Collection, CollectionSearchResult, StoryWithCollectionContext, CollectionStatistics } from '../types/api';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || '/api';
@@ -136,6 +136,11 @@ export const storyApi = {
return response.data;
},
getStoryCollections: async (storyId: string): Promise<Collection[]> => {
const response = await api.get(`/stories/${storyId}/collections`);
return response.data;
},
reindexTypesense: async (): Promise<{ success: boolean; message: string; count?: number; error?: string }> => {
const response = await api.post('/stories/reindex-typesense');
return response.data;
@@ -307,6 +312,141 @@ export const configApi = {
},
};
// Collection endpoints
export const collectionApi = {
getCollections: async (params?: {
page?: number;
limit?: number;
search?: string;
tags?: string[];
archived?: boolean;
}): Promise<CollectionSearchResult> => {
// Create URLSearchParams to properly handle array parameters
const searchParams = new URLSearchParams();
if (params?.page !== undefined) searchParams.append('page', params.page.toString());
if (params?.limit !== undefined) searchParams.append('limit', params.limit.toString());
if (params?.search) searchParams.append('search', params.search);
if (params?.archived !== undefined) searchParams.append('archived', params.archived.toString());
// Add array parameters - each element gets its own parameter
if (params?.tags && params.tags.length > 0) {
params.tags.forEach(tag => searchParams.append('tags', tag));
}
const response = await api.get(`/collections?${searchParams.toString()}`);
return response.data;
},
getCollection: async (id: string): Promise<Collection> => {
const response = await api.get(`/collections/${id}`);
return response.data;
},
createCollection: async (collectionData: {
name: string;
description?: string;
tagNames?: string[];
storyIds?: string[];
}): Promise<Collection> => {
const response = await api.post('/collections', collectionData);
return response.data;
},
createCollectionWithImage: async (collectionData: {
name: string;
description?: string;
tags?: string[];
storyIds?: string[];
coverImage?: File;
}): Promise<Collection> => {
const formData = new FormData();
formData.append('name', collectionData.name);
if (collectionData.description) formData.append('description', collectionData.description);
if (collectionData.tags) {
collectionData.tags.forEach(tag => formData.append('tags', tag));
}
if (collectionData.storyIds) {
collectionData.storyIds.forEach(id => formData.append('storyIds', id));
}
if (collectionData.coverImage) {
formData.append('coverImage', collectionData.coverImage);
}
const response = await api.post('/collections', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
},
updateCollection: async (id: string, collectionData: {
name?: string;
description?: string;
tagNames?: string[];
rating?: number;
}): Promise<Collection> => {
const response = await api.put(`/collections/${id}`, collectionData);
return response.data;
},
deleteCollection: async (id: string): Promise<void> => {
await api.delete(`/collections/${id}`);
},
archiveCollection: async (id: string, archived: boolean): Promise<Collection> => {
const response = await api.put(`/collections/${id}/archive`, { archived });
return response.data;
},
addStoriesToCollection: async (id: string, storyIds: string[], position?: number): Promise<{
added: number;
skipped: number;
totalStories: number;
}> => {
const response = await api.post(`/collections/${id}/stories`, {
storyIds,
position,
});
return response.data;
},
removeStoryFromCollection: async (collectionId: string, storyId: string): Promise<void> => {
await api.delete(`/collections/${collectionId}/stories/${storyId}`);
},
reorderStories: async (collectionId: string, storyOrders: Array<{
storyId: string;
position: number;
}>): Promise<void> => {
await api.put(`/collections/${collectionId}/stories/order`, {
storyOrders,
});
},
getStoryWithCollectionContext: async (collectionId: string, storyId: string): Promise<StoryWithCollectionContext> => {
const response = await api.get(`/collections/${collectionId}/read/${storyId}`);
return response.data;
},
getCollectionStatistics: async (id: string): Promise<CollectionStatistics> => {
const response = await api.get(`/collections/${id}/stats`);
return response.data;
},
uploadCover: async (id: string, coverImage: File): Promise<{ imagePath: string }> => {
const formData = new FormData();
formData.append('file', coverImage);
const response = await api.post(`/collections/${id}/cover`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
},
removeCover: async (id: string): Promise<void> => {
await api.delete(`/collections/${id}/cover`);
},
};
// Image utility
export const getImageUrl = (path: string): string => {
if (!path) return '';