Various Fixes and QoL enhancements.
This commit is contained in:
@@ -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
|
||||
|
||||
33
frontend/src/lib/settings.ts
Normal file
33
frontend/src/lib/settings.ts
Normal 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));
|
||||
};
|
||||
Reference in New Issue
Block a user