From f0ea71e00e82b72784d6d44565e6e9ec2e9a1ace Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 10 Dec 2025 20:37:30 -0500 Subject: [PATCH] [FA-misc] Various UI updates --- .../Program.cs | 1 - .../Extensions/GraphQLExtensions.cs | 1 + .../src/lib/components/Navbar.svelte | 4 +- .../src/lib/components/NovelCard.svelte | 6 + .../src/lib/components/NovelDetailPage.svelte | 40 ++++- .../src/lib/components/NovelFilters.svelte | 56 ++++++- .../src/lib/components/NovelsPage.svelte | 13 +- .../components/RecentlyUpdatedSection.svelte | 7 +- .../src/lib/components/SearchBar.svelte | 141 ++++++++++++++++++ .../src/lib/constants/systemTags.ts | 3 + .../src/lib/graphql/__generated__/graphql.ts | 5 +- .../src/lib/graphql/queries/novels.graphql | 5 +- .../src/lib/utils/filterParams.ts | 44 +++++- 13 files changed, 298 insertions(+), 28 deletions(-) create mode 100644 fictionarchive-web-astro/src/lib/components/SearchBar.svelte create mode 100644 fictionarchive-web-astro/src/lib/constants/systemTags.ts diff --git a/FictionArchive.Service.NovelService/Program.cs b/FictionArchive.Service.NovelService/Program.cs index 6a45dba..0b1f860 100644 --- a/FictionArchive.Service.NovelService/Program.cs +++ b/FictionArchive.Service.NovelService/Program.cs @@ -44,7 +44,6 @@ public class Program #region GraphQL builder.Services.AddDefaultGraphQl() - .ModifyCostOptions(opt => opt.MaxFieldCost = 5000) .AddAuthorization(); #endregion diff --git a/FictionArchive.Service.Shared/Extensions/GraphQLExtensions.cs b/FictionArchive.Service.Shared/Extensions/GraphQLExtensions.cs index d329975..22a3f14 100644 --- a/FictionArchive.Service.Shared/Extensions/GraphQLExtensions.cs +++ b/FictionArchive.Service.Shared/Extensions/GraphQLExtensions.cs @@ -22,6 +22,7 @@ public static class GraphQLExtensions .AddErrorFilter() .AddType() .AddType() + .ModifyCostOptions(opt => opt.MaxFieldCost = 10000) .AddMutationConventions(applyToAllMutations: true) .AddFiltering(opt => opt.AddDefaults().BindRuntimeType()) .AddSorting() diff --git a/fictionarchive-web-astro/src/lib/components/Navbar.svelte b/fictionarchive-web-astro/src/lib/components/Navbar.svelte index 94d1f24..459c00f 100644 --- a/fictionarchive-web-astro/src/lib/components/Navbar.svelte +++ b/fictionarchive-web-astro/src/lib/components/Navbar.svelte @@ -1,7 +1,7 @@ {statusLabel} + {#if isNsfw} + NSFW + {/if} diff --git a/fictionarchive-web-astro/src/lib/components/NovelDetailPage.svelte b/fictionarchive-web-astro/src/lib/components/NovelDetailPage.svelte index 6cf00ee..b38c535 100644 --- a/fictionarchive-web-astro/src/lib/components/NovelDetailPage.svelte +++ b/fictionarchive-web-astro/src/lib/components/NovelDetailPage.svelte @@ -1,5 +1,7 @@ + + diff --git a/fictionarchive-web-astro/src/lib/constants/systemTags.ts b/fictionarchive-web-astro/src/lib/constants/systemTags.ts new file mode 100644 index 0000000..2a26155 --- /dev/null +++ b/fictionarchive-web-astro/src/lib/constants/systemTags.ts @@ -0,0 +1,3 @@ +export const SystemTags = { + Nsfw: 'Nsfw' +} as const; diff --git a/fictionarchive-web-astro/src/lib/graphql/__generated__/graphql.ts b/fictionarchive-web-astro/src/lib/graphql/__generated__/graphql.ts index 80c0b26..f6254c8 100644 --- a/fictionarchive-web-astro/src/lib/graphql/__generated__/graphql.ts +++ b/fictionarchive-web-astro/src/lib/graphql/__generated__/graphql.ts @@ -733,13 +733,14 @@ export type NovelsQueryVariables = Exact<{ first?: InputMaybe; after?: InputMaybe; where?: InputMaybe; + order?: InputMaybe | NovelDtoSortInput>; }>; -export type NovelsQuery = { novels: { edges: Array<{ cursor: string, node: { id: any, url: string, name: string, description: string, rawStatus: NovelStatus, lastUpdatedTime: any, coverImage: { newPath: string | null } | null, chapters: Array<{ order: any, name: string }>, tags: Array<{ key: string, displayName: string }> } }> | null, pageInfo: { hasNextPage: boolean, endCursor: string | null } } | null }; +export type NovelsQuery = { novels: { edges: Array<{ cursor: string, node: { id: any, url: string, name: string, description: string, rawStatus: NovelStatus, lastUpdatedTime: any, coverImage: { newPath: string | null } | null, chapters: Array<{ order: any, name: string }>, tags: Array<{ key: string, displayName: string, tagType: TagType }> } }> | null, pageInfo: { hasNextPage: boolean, endCursor: string | null } } | null }; export const ImportNovelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ImportNovel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ImportNovelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"importNovel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novelUpdateRequestedEvent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novelUrl"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetChapterDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetChapter"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"novelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"chapterOrder"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"novelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"novelId"}}},{"kind":"Argument","name":{"kind":"Name","value":"chapterOrder"},"value":{"kind":"Variable","name":{"kind":"Name","value":"chapterOrder"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"revision"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"images"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}},{"kind":"Field","name":{"kind":"Name","value":"novelId"}},{"kind":"Field","name":{"kind":"Name","value":"novelName"}},{"kind":"Field","name":{"kind":"Name","value":"totalChapters"}},{"kind":"Field","name":{"kind":"Name","value":"prevChapterOrder"}},{"kind":"Field","name":{"kind":"Name","value":"nextChapterOrder"}}]}}]}}]} as unknown as DocumentNode; export const NovelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Novel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}},{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"rawLanguage"}},{"kind":"Field","name":{"kind":"Name","value":"rawStatus"}},{"kind":"Field","name":{"kind":"Name","value":"statusOverride"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"externalUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"source"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"coverImage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"tagType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"chapters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"images"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const NovelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Novels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"NovelDtoFilterInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"coverImage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}},{"kind":"Field","name":{"kind":"Name","value":"rawStatus"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"chapters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const NovelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Novels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"NovelDtoFilterInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"order"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"NovelDtoSortInput"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}},{"kind":"Argument","name":{"kind":"Name","value":"order"},"value":{"kind":"Variable","name":{"kind":"Name","value":"order"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"coverImage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}},{"kind":"Field","name":{"kind":"Name","value":"rawStatus"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"chapters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"tagType"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/fictionarchive-web-astro/src/lib/graphql/queries/novels.graphql b/fictionarchive-web-astro/src/lib/graphql/queries/novels.graphql index 5b5dab9..15292dc 100644 --- a/fictionarchive-web-astro/src/lib/graphql/queries/novels.graphql +++ b/fictionarchive-web-astro/src/lib/graphql/queries/novels.graphql @@ -1,5 +1,5 @@ -query Novels($first: Int, $after: String, $where: NovelDtoFilterInput) { - novels(first: $first, after: $after, where: $where) { +query Novels($first: Int, $after: String, $where: NovelDtoFilterInput, $order: [NovelDtoSortInput!]) { + novels(first: $first, after: $after, where: $where, order: $order) { edges { cursor node { @@ -19,6 +19,7 @@ query Novels($first: Int, $after: String, $where: NovelDtoFilterInput) { tags { key displayName + tagType } } } diff --git a/fictionarchive-web-astro/src/lib/utils/filterParams.ts b/fictionarchive-web-astro/src/lib/utils/filterParams.ts index ac4512e..23bf578 100644 --- a/fictionarchive-web-astro/src/lib/utils/filterParams.ts +++ b/fictionarchive-web-astro/src/lib/utils/filterParams.ts @@ -1,20 +1,37 @@ -import type { NovelDtoFilterInput, NovelStatus } from '$lib/graphql/__generated__/graphql'; +import type { NovelDtoFilterInput, NovelDtoSortInput, NovelStatus, SortEnumType } from '$lib/graphql/__generated__/graphql'; + +export type SortField = 'lastUpdatedTime' | 'createdTime' | 'name'; +export type SortDirection = SortEnumType; + +export interface NovelSort { + field: SortField; + direction: SortDirection; +} export interface NovelFilters { search: string; statuses: NovelStatus[]; tags: string[]; authorName: string; + sort: NovelSort; } +export const DEFAULT_SORT: NovelSort = { + field: 'lastUpdatedTime', + direction: 'DESC' +}; + export const EMPTY_FILTERS: NovelFilters = { search: '', statuses: [], tags: [], - authorName: '' + authorName: '', + sort: DEFAULT_SORT }; const VALID_STATUSES: NovelStatus[] = ['ABANDONED', 'COMPLETED', 'HIATUS', 'IN_PROGRESS', 'UNKNOWN']; +const VALID_SORT_FIELDS: SortField[] = ['lastUpdatedTime', 'createdTime', 'name']; +const VALID_SORT_DIRECTIONS: SortDirection[] = ['ASC', 'DESC']; /** * Parse filter state from URL search parameters @@ -34,7 +51,15 @@ export function parseFiltersFromURL(searchParams?: URLSearchParams): NovelFilter const authorName = params.get('author') ?? ''; - return { search, statuses, tags, authorName }; + // Parse sort parameters + const sortField = params.get('sortBy') as SortField | null; + const sortDir = params.get('sortDir') as SortDirection | null; + const sort: NovelSort = { + field: sortField && VALID_SORT_FIELDS.includes(sortField) ? sortField : DEFAULT_SORT.field, + direction: sortDir && VALID_SORT_DIRECTIONS.includes(sortDir) ? sortDir : DEFAULT_SORT.direction + }; + + return { search, statuses, tags, authorName, sort }; } /** @@ -59,6 +84,12 @@ export function filtersToURLParams(filters: NovelFilters): string { params.set('author', filters.authorName.trim()); } + // Only include sort params if different from default + if (filters.sort.field !== DEFAULT_SORT.field || filters.sort.direction !== DEFAULT_SORT.direction) { + params.set('sortBy', filters.sort.field); + params.set('sortDir', filters.sort.direction); + } + return params.toString(); } @@ -135,3 +166,10 @@ export function hasActiveFilters(filters: NovelFilters): boolean { filters.authorName.trim().length > 0 ); } + +/** + * Convert sort state to GraphQL order input + */ +export function sortToGraphQLOrder(sort: NovelSort): NovelDtoSortInput[] { + return [{ [sort.field]: sort.direction }]; +} -- 2.49.1