Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/web/components/EpisodesPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ describe("EpisodesPage (#439)", () => {
it("lists episodes in reader order with Genesis as Episode 1", async () => {
render(<EpisodesPage storyName="god-cell" authFetch={makeAuthFetch()} onOpenFile={vi.fn()} />);
expect(await screen.findByTestId("episodes-page")).toBeInTheDocument();
expect(screen.getByTestId("episodes-summary")).toHaveTextContent("2 total");
expect(screen.getByTestId("episodes-summary")).toHaveTextContent("1 ready");
expect(screen.getByTestId("episodes-row-genesis.md")).toHaveTextContent("Episode 1 / Genesis");
expect(screen.getByTestId("episodes-row-plot-01.md")).toHaveTextContent("Episode 2");
});
Expand Down
24 changes: 24 additions & 0 deletions app/web/components/EpisodesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,34 @@ export function EpisodesPage({ storyName, authFetch, onOpenFile }: EpisodesPageP
return <div className="h-full flex items-center justify-center text-muted text-sm">Could not load episodes.</div>;
}

const publishedCount = episodes.filter((ep) => ep.published).length;
const activeCount = episodes.filter((ep) => !ep.published).length;
const blockedCount = episodes.filter((ep) => ep.state === "blocked").length;
const readyCount = episodes.filter((ep) => ep.state === "ready").length;

return (
<div className="h-full overflow-y-auto px-4 py-4" data-testid="episodes-page">
<h2 className="text-base font-serif text-foreground">Episodes</h2>
<p className="mt-0.5 text-[11px] text-muted">Genesis is Episode 1; each plot file is the next episode.</p>
<div className="mt-3 flex flex-wrap gap-1.5 text-[10px]" data-testid="episodes-summary">
<span className="rounded-full border border-border bg-background px-2 py-0.5 text-foreground">
{episodes.length} total
</span>
<span className="rounded-full border border-border bg-background px-2 py-0.5 text-muted">
{activeCount} active
</span>
<span className="rounded-full border border-green-700/30 bg-green-700/10 px-2 py-0.5 text-green-700">
{publishedCount} published
</span>
<span className="rounded-full border border-accent/30 bg-accent/10 px-2 py-0.5 text-accent">
{readyCount} ready
</span>
{blockedCount > 0 && (
<span className="rounded-full border border-error/30 bg-error/10 px-2 py-0.5 text-error">
{blockedCount} need fixes
</span>
)}
</div>

{episodes.length === 0 ? (
<p className="mt-4 text-xs text-muted italic" data-testid="episodes-empty">No episodes yet — write the Genesis to start Episode 1.</p>
Expand Down
1 change: 1 addition & 0 deletions app/web/components/StoriesPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,7 @@ describe("StoriesPage cartoon workflow nav routing (#439)", () => {
expect(
within(publishCta).getByRole("button", { name: "Next Action" }),
).toBeInTheDocument();
expect(publishCta.className).toContain("absolute");
expect(
within(publishCta).queryByText("No next action available"),
).not.toBeInTheDocument();
Expand Down
20 changes: 11 additions & 9 deletions app/web/components/StoriesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1300,17 +1300,19 @@ export function StoriesPage({ token, authFetch }: StoriesPageProps) {
</div>
{!focusedLetteringMode && isCartoonStory && selectedStory && (
<div
className="flex-shrink-0 border-t border-border bg-background/95 backdrop-blur"
className="pointer-events-none absolute bottom-4 right-4 z-10 w-[min(22rem,calc(100%-2rem))]"
data-testid="workflow-persistent-next-action"
>
<CartoonNextAction
storyName={selectedStory}
fileName={cartoonView === null ? selectedFile : null}
authFetch={authFetch}
refreshKey={cartoonPublishRefresh}
onCoachAction={handleWorkflowNextAction}
onOpenStoryInfo={() => setCartoonView("story-info")}
/>
<div className="pointer-events-auto rounded-xl border border-border/80 bg-background/95 shadow-lg backdrop-blur">
<CartoonNextAction
storyName={selectedStory}
fileName={cartoonView === null ? selectedFile : null}
authFetch={authFetch}
refreshKey={cartoonPublishRefresh}
onCoachAction={handleWorkflowNextAction}
onOpenStoryInfo={() => setCartoonView("story-info")}
/>
</div>
</div>
)}
{publishProgress && (
Expand Down
45 changes: 31 additions & 14 deletions app/web/components/StoryInfoPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,37 @@ export function StoryInfoPage({ storyName, authFetch, onSaved }: StoryInfoPagePr

return (
<div className="h-full overflow-y-auto px-4 py-4" data-testid="story-info-page">
<h2 className="text-base font-serif text-foreground">Story Info</h2>
<p className="mt-0.5 text-[11px] text-muted">These details appear on PlotLink when the story is published.</p>
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<h2 className="text-base font-serif text-foreground">Story Info</h2>
<p className="mt-0.5 text-[11px] text-muted">These details appear on PlotLink when the story is published.</p>
<div className="mt-2 flex flex-wrap gap-1.5 text-[10px]">
<span className={`rounded-full border px-2 py-0.5 ${title.trim() ? "border-green-700/30 bg-green-700/10 text-green-700" : "border-border bg-background text-muted"}`}>
Title {title.trim() ? "ready" : "missing"}
</span>
<span className={`rounded-full border px-2 py-0.5 ${genre ? "border-green-700/30 bg-green-700/10 text-green-700" : "border-border bg-background text-muted"}`}>
Genre {genre ? "set" : "needed"}
</span>
<span className={`rounded-full border px-2 py-0.5 ${language ? "border-green-700/30 bg-green-700/10 text-green-700" : "border-border bg-background text-muted"}`}>
Language {language ? "set" : "needed"}
</span>
<span className={`rounded-full border px-2 py-0.5 ${cover === "present" ? "border-green-700/30 bg-green-700/10 text-green-700" : "border-border bg-background text-muted"}`}>
{cover === "present" ? "Cover ready" : "Cover missing"}
</span>
</div>
</div>
<div className="flex flex-col items-start gap-1.5">
<button
type="button" onClick={handleSave} disabled={saving}
data-testid="story-info-save"
className="rounded bg-accent px-3 py-1.5 text-xs font-medium text-white hover:bg-accent-dim transition-colors disabled:opacity-50"
>
{saving ? "Saving…" : "Save Story Info"}
</button>
{saved && <span className="text-[11px] text-green-700" data-testid="story-info-saved">Saved</span>}
{saveError && <span className="text-[11px] text-error" data-testid="story-info-error">{saveError}</span>}
</div>
</div>

<div className="mt-4 flex flex-col gap-4 max-w-xl">
<label className="flex flex-col gap-1">
Expand Down Expand Up @@ -248,18 +277,6 @@ export function StoryInfoPage({ storyName, authFetch, onSaved }: StoryInfoPagePr
/>
<span className="text-xs text-foreground">This story contains adult content (18+)</span>
</label>

<div className="flex items-center gap-3">
<button
type="button" onClick={handleSave} disabled={saving}
data-testid="story-info-save"
className="rounded bg-accent px-3 py-1.5 text-xs font-medium text-white hover:bg-accent-dim transition-colors disabled:opacity-50"
>
{saving ? "Saving…" : "Save Story Info"}
</button>
{saved && <span className="text-[11px] text-green-700" data-testid="story-info-saved">Saved</span>}
{saveError && <span className="text-[11px] text-error" data-testid="story-info-error">{saveError}</span>}
</div>
</div>
</div>
);
Expand Down
Loading