Various Fixes and QoL enhancements.

This commit is contained in:
Stefan Hardegger
2025-07-26 12:05:54 +02:00
parent 5e8164c6a4
commit f95d7aa8bb
32 changed files with 758 additions and 136 deletions

View File

@@ -21,15 +21,36 @@ api.interceptors.request.use((config) => {
return config;
});
// Global auth failure handler - can be set by AuthContext
let globalAuthFailureHandler: (() => void) | null = null;
export const setGlobalAuthFailureHandler = (handler: () => void) => {
globalAuthFailureHandler = handler;
};
// Response interceptor to handle auth errors
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Clear invalid token and redirect to login
// Handle authentication failures
if (error.response?.status === 401 || error.response?.status === 403) {
console.warn('Authentication failed, token may be expired or invalid');
// Clear invalid token
localStorage.removeItem('auth-token');
window.location.href = '/login';
// Use global handler if available (from AuthContext), otherwise fallback to direct redirect
if (globalAuthFailureHandler) {
globalAuthFailureHandler();
} else {
// Fallback for cases where AuthContext isn't available
window.location.href = '/login';
}
// Return a more specific error for components to handle gracefully
return Promise.reject(new Error('Authentication required'));
}
return Promise.reject(error);
}
);
@@ -150,6 +171,22 @@ export const storyApi = {
const response = await api.post('/stories/recreate-typesense-collection');
return response.data;
},
checkDuplicate: async (title: string, authorName: string): Promise<{
hasDuplicates: boolean;
count: number;
duplicates: Array<{
id: string;
title: string;
authorName: string;
createdAt: string;
}>;
}> => {
const response = await api.get('/stories/check-duplicate', {
params: { title, authorName }
});
return response.data;
},
};
// Author endpoints
@@ -240,6 +277,11 @@ export const tagApi = {
// Backend returns TagDto[], extract just the names
return response.data.map((tag: Tag) => tag.name);
},
getCollectionTags: async (): Promise<Tag[]> => {
const response = await api.get('/tags/collections');
return response.data;
},
};
// Series endpoints

View File

@@ -0,0 +1,33 @@
interface Settings {
theme: 'light' | 'dark';
fontFamily: 'serif' | 'sans' | 'mono';
fontSize: 'small' | 'medium' | 'large' | 'extra-large';
readingWidth: 'narrow' | 'medium' | 'wide';
readingSpeed: number; // words per minute
}
const defaultSettings: Settings = {
theme: 'light',
fontFamily: 'serif',
fontSize: 'medium',
readingWidth: 'medium',
readingSpeed: 200,
};
export const getReadingSpeed = (): number => {
try {
const savedSettings = localStorage.getItem('storycove-settings');
if (savedSettings) {
const parsed = JSON.parse(savedSettings);
return parsed.readingSpeed || defaultSettings.readingSpeed;
}
} catch (error) {
console.error('Failed to parse saved settings:', error);
}
return defaultSettings.readingSpeed;
};
export const calculateReadingTime = (wordCount: number): number => {
const wordsPerMinute = getReadingSpeed();
return Math.max(1, Math.round(wordCount / wordsPerMinute));
};