[FA-misc] Switches to using DTOs, updates frontend with details and reader page, updates novel import to be an upsert
This commit is contained in:
115
fictionarchive-web-astro/src/lib/utils/filterParams.ts
Normal file
115
fictionarchive-web-astro/src/lib/utils/filterParams.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import type { NovelDtoFilterInput, NovelStatus } from '$lib/graphql/__generated__/graphql';
|
||||
|
||||
export interface NovelFilters {
|
||||
search: string;
|
||||
statuses: NovelStatus[];
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export const EMPTY_FILTERS: NovelFilters = {
|
||||
search: '',
|
||||
statuses: [],
|
||||
tags: []
|
||||
};
|
||||
|
||||
const VALID_STATUSES: NovelStatus[] = ['ABANDONED', 'COMPLETED', 'HIATUS', 'IN_PROGRESS', 'UNKNOWN'];
|
||||
|
||||
/**
|
||||
* Parse filter state from URL search parameters
|
||||
*/
|
||||
export function parseFiltersFromURL(searchParams?: URLSearchParams): NovelFilters {
|
||||
const params = searchParams ?? new URLSearchParams(window.location.search);
|
||||
|
||||
const search = params.get('search') ?? '';
|
||||
|
||||
const statusParam = params.get('status') ?? '';
|
||||
const statuses = statusParam
|
||||
.split(',')
|
||||
.filter((s) => s && VALID_STATUSES.includes(s as NovelStatus)) as NovelStatus[];
|
||||
|
||||
const tagsParam = params.get('tags') ?? '';
|
||||
const tags = tagsParam.split(',').filter((t) => t.length > 0);
|
||||
|
||||
return { search, statuses, tags };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert filter state to URL search params string
|
||||
*/
|
||||
export function filtersToURLParams(filters: NovelFilters): string {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (filters.search.trim()) {
|
||||
params.set('search', filters.search.trim());
|
||||
}
|
||||
|
||||
if (filters.statuses.length > 0) {
|
||||
params.set('status', filters.statuses.join(','));
|
||||
}
|
||||
|
||||
if (filters.tags.length > 0) {
|
||||
params.set('tags', filters.tags.join(','));
|
||||
}
|
||||
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update browser URL with current filters (without page reload)
|
||||
*/
|
||||
export function syncFiltersToURL(filters: NovelFilters): void {
|
||||
const params = filtersToURLParams(filters);
|
||||
const newUrl = params ? `${window.location.pathname}?${params}` : window.location.pathname;
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert filter state to GraphQL where input
|
||||
* Returns null if no filters are active
|
||||
*/
|
||||
export function filtersToGraphQLWhere(filters: NovelFilters): NovelDtoFilterInput | null {
|
||||
const conditions: NovelDtoFilterInput[] = [];
|
||||
|
||||
// Text search on name (case-insensitive contains)
|
||||
if (filters.search.trim()) {
|
||||
conditions.push({
|
||||
name: { contains: filters.search.trim() }
|
||||
});
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if (filters.statuses.length > 0) {
|
||||
conditions.push({
|
||||
rawStatus: { in: filters.statuses }
|
||||
});
|
||||
}
|
||||
|
||||
// Tag filter (match novels that have ANY of the selected tags)
|
||||
if (filters.tags.length > 0) {
|
||||
conditions.push({
|
||||
tags: {
|
||||
some: {
|
||||
key: { in: filters.tags }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Return null if no filters, single condition if one filter, AND for multiple
|
||||
if (conditions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (conditions.length === 1) {
|
||||
return conditions[0];
|
||||
}
|
||||
|
||||
return { and: conditions };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any filters are active
|
||||
*/
|
||||
export function hasActiveFilters(filters: NovelFilters): boolean {
|
||||
return filters.search.trim().length > 0 || filters.statuses.length > 0 || filters.tags.length > 0;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
|
||||
/**
|
||||
* Sanitizes HTML content, allowing only safe inline formatting elements.
|
||||
|
||||
74
fictionarchive-web-astro/src/lib/utils/sanitizeChapter.ts
Normal file
74
fictionarchive-web-astro/src/lib/utils/sanitizeChapter.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
|
||||
/**
|
||||
* Sanitizes chapter HTML content with extended allowed tags.
|
||||
* More permissive than the description sanitizer to support
|
||||
* formatted novel content including headings, lists, and images.
|
||||
*/
|
||||
export function sanitizeChapterHtml(html: string): string {
|
||||
return DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: [
|
||||
// Basic formatting
|
||||
'b',
|
||||
'i',
|
||||
'em',
|
||||
'strong',
|
||||
'u',
|
||||
's',
|
||||
'strike',
|
||||
'del',
|
||||
'ins',
|
||||
// Structure
|
||||
'p',
|
||||
'br',
|
||||
'hr',
|
||||
'div',
|
||||
'span',
|
||||
// Headings
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
// Lists
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
// Quotes
|
||||
'blockquote',
|
||||
'q',
|
||||
'cite',
|
||||
// Preformatted
|
||||
'pre',
|
||||
'code',
|
||||
// Ruby (for Asian language annotations)
|
||||
'ruby',
|
||||
'rt',
|
||||
'rp',
|
||||
// Images
|
||||
'img',
|
||||
// Tables
|
||||
'table',
|
||||
'thead',
|
||||
'tbody',
|
||||
'tr',
|
||||
'th',
|
||||
'td'
|
||||
],
|
||||
ALLOWED_ATTR: [
|
||||
// Image attributes
|
||||
'src',
|
||||
'alt',
|
||||
'title',
|
||||
'width',
|
||||
'height',
|
||||
// Table attributes
|
||||
'colspan',
|
||||
'rowspan',
|
||||
// Generic styling (limited)
|
||||
'class'
|
||||
],
|
||||
ALLOW_DATA_ATTR: false
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user