From 3795ddd541aee4e03c87f052173585b15cc7f965 Mon Sep 17 00:00:00 2001 From: James Wampler Date: Wed, 19 Aug 2026 18:07:21 -0700 Subject: [PATCH] Add chapter kind to the web client Chapter and its cross-referencing chips (tags, locations, questions, character arcs) now carry kind/displayNumber/label fields end to end. Chapter detail page gets a Kind selector; chapter chips across the app render "Foreword"/"Afterword" instead of a misleading number for front and back matter. --- src/Novelly.Web/src/api/chapterLabel.ts | 6 +++++ src/Novelly.Web/src/api/types.ts | 15 ++++++++++-- .../src/components/CharacterArc.tsx | 7 +++--- .../src/components/OpenQuestions.tsx | 2 +- src/Novelly.Web/src/components/ui.tsx | 4 +++- src/Novelly.Web/src/pages/ChapterPage.tsx | 24 +++++++++++++------ src/Novelly.Web/src/pages/ChaptersPage.tsx | 2 +- .../src/pages/CharacterDetailPage.tsx | 11 +++++---- src/Novelly.Web/src/pages/DashboardPage.tsx | 2 +- src/Novelly.Web/src/pages/LocationsPage.tsx | 3 ++- src/Novelly.Web/src/pages/TagsPage.tsx | 5 ++-- 11 files changed, 57 insertions(+), 24 deletions(-) create mode 100644 src/Novelly.Web/src/api/chapterLabel.ts diff --git a/src/Novelly.Web/src/api/chapterLabel.ts b/src/Novelly.Web/src/api/chapterLabel.ts new file mode 100644 index 0000000..9a295c5 --- /dev/null +++ b/src/Novelly.Web/src/api/chapterLabel.ts @@ -0,0 +1,6 @@ +import type { ChapterKind } from './types' + +export function chapterLabel(kind: ChapterKind, displayNumber: number | null, title: string): string { + if (kind === 'Body' && displayNumber !== null) return `Chapter ${displayNumber}: ${title}` + return title +} diff --git a/src/Novelly.Web/src/api/types.ts b/src/Novelly.Web/src/api/types.ts index 47615a3..5741f25 100644 --- a/src/Novelly.Web/src/api/types.ts +++ b/src/Novelly.Web/src/api/types.ts @@ -28,6 +28,10 @@ export type DraftStatus = 'Planned' | 'Outlined' | 'Drafted' | 'Revised' | 'Fina export const draftStatuses: DraftStatus[] = ['Planned', 'Outlined', 'Drafted', 'Revised', 'Final'] +export type ChapterKind = 'FrontMatter' | 'Body' | 'BackMatter' + +export const chapterKinds: ChapterKind[] = ['FrontMatter', 'Body', 'BackMatter'] + export type NovelPhase = 'Brainstorming' | 'Outlining' | 'Writing' | 'Editing' | 'Complete' export const novelPhases: NovelPhase[] = ['Brainstorming', 'Outlining', 'Writing', 'Editing', 'Complete'] @@ -112,12 +116,13 @@ export interface TagSummary extends Tag { export interface TagReferences { tag: Tag characters: { id: string; name: string; role: string }[] - chapters: { id: string; number: number; title: string; summary: string | null }[] + chapters: { id: string; number: number; kind: ChapterKind; displayNumber: number | null; title: string; summary: string | null }[] beats: { id: string chapterId: string chapterNumber: number chapterTitle: string + chapterLabel: string sortOrder: number title: string characterName: string | null @@ -136,7 +141,7 @@ export interface LocationSummary extends Location { export interface LocationReferences { location: Location - chapters: { id: string; number: number; title: string; summary: string | null }[] + chapters: { id: string; number: number; kind: ChapterKind; displayNumber: number | null; title: string; summary: string | null }[] } export interface BeatCharacter { @@ -173,6 +178,7 @@ export interface ArcStage { chapterId: string | null chapterNumber: number | null chapterTitle: string | null + chapterLabel: string | null beats: CharacterBeat[] updatedAt: string } @@ -182,6 +188,7 @@ export interface CharacterBeat { chapterId: string chapterNumber: number chapterTitle: string + chapterLabel: string sortOrder: number title: string whatHappened: string | null @@ -210,6 +217,7 @@ export interface Character { sameCharacterAsName: string | null revealedInChapterId: string | null revealedInChapterNumber: number | null + revealedInChapterLabel: string | null identityNote: string | null otherIdentities: CharacterIdentity[] relationships: Relationship[] @@ -227,6 +235,8 @@ export interface ChapterSummary { id: string novelId: string number: number + kind: ChapterKind + displayNumber: number | null title: string summary: string | null locations: Location[] @@ -254,6 +264,7 @@ export interface OpenQuestion { chapterId: string | null chapterNumber: number | null chapterTitle: string | null + chapterLabel: string | null characterId: string | null characterName: string | null resolution: string | null diff --git a/src/Novelly.Web/src/components/CharacterArc.tsx b/src/Novelly.Web/src/components/CharacterArc.tsx index a8e05aa..4225177 100644 --- a/src/Novelly.Web/src/components/CharacterArc.tsx +++ b/src/Novelly.Web/src/components/CharacterArc.tsx @@ -9,7 +9,8 @@ import { useSetArcStageBeats, useUpdateArcStage, } from '../api/hooks' -import type { ArcStage, Character } from '../api/types' +import { chapterLabel } from '../api/chapterLabel' +import type { ArcStage, Character, ChapterKind } from '../api/types' import { AutoField, ErrorNote } from './ui' export function CharacterArc({ @@ -127,7 +128,7 @@ function ArcStageRow({ }: { novelId: string stage: ArcStage - chapters: { id: string; number: number; title: string }[] + chapters: { id: string; number: number; kind: ChapterKind; displayNumber: number | null; title: string }[] unassignedBeats: { id: string; chapterNumber: number; sortOrder: number; title: string }[] canMoveUp: boolean canMoveDown: boolean @@ -226,7 +227,7 @@ function ArcStageRow({ {chapters.map((chapter) => ( ))} diff --git a/src/Novelly.Web/src/components/OpenQuestions.tsx b/src/Novelly.Web/src/components/OpenQuestions.tsx index be4a672..b39ad0e 100644 --- a/src/Novelly.Web/src/components/OpenQuestions.tsx +++ b/src/Novelly.Web/src/components/OpenQuestions.tsx @@ -182,7 +182,7 @@ function QuestionRow({ {(showsChapter || showsCharacter) && (

- {showsChapter && `Ch. ${question.chapterNumber} ${question.chapterTitle}`} + {showsChapter && question.chapterLabel} {showsChapter && showsCharacter && ' · '} {showsCharacter && question.characterName}

diff --git a/src/Novelly.Web/src/components/ui.tsx b/src/Novelly.Web/src/components/ui.tsx index 452758c..9372ffa 100644 --- a/src/Novelly.Web/src/components/ui.tsx +++ b/src/Novelly.Web/src/components/ui.tsx @@ -139,11 +139,13 @@ export function AutoField({ } export function Select({ + id, label, value, options, onChange, }: { + id?: string label?: string value: T options: readonly T[] @@ -152,7 +154,7 @@ export function Select({ return (