mirror of
https://github.com/Narcooo/inkos.git
synced 2026-05-06 21:43:26 +08:00
fix(studio): stabilize book creation defaults
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useApi, postApi } from "../hooks/use-api";
|
||||
import type { Theme } from "../hooks/use-theme";
|
||||
import type { TFunction } from "../hooks/use-i18n";
|
||||
@@ -16,20 +16,40 @@ interface GenreInfo {
|
||||
readonly language: "zh" | "en";
|
||||
}
|
||||
|
||||
const PLATFORMS_ZH = [
|
||||
interface PlatformOption {
|
||||
readonly value: string;
|
||||
readonly label: string;
|
||||
}
|
||||
|
||||
const PLATFORMS_ZH: ReadonlyArray<PlatformOption> = [
|
||||
{ value: "tomato", label: "番茄小说" },
|
||||
{ value: "qidian", label: "起点中文网" },
|
||||
{ value: "feilu", label: "飞卢" },
|
||||
{ value: "other", label: "其他" },
|
||||
];
|
||||
|
||||
const PLATFORMS_EN = [
|
||||
{ value: "other", label: "Royal Road" },
|
||||
{ value: "other", label: "Kindle Unlimited" },
|
||||
{ value: "other", label: "Scribble Hub" },
|
||||
const PLATFORMS_EN: ReadonlyArray<PlatformOption> = [
|
||||
{ value: "royal-road", label: "Royal Road" },
|
||||
{ value: "kindle-unlimited", label: "Kindle Unlimited" },
|
||||
{ value: "scribble-hub", label: "Scribble Hub" },
|
||||
{ value: "other", label: "Other" },
|
||||
];
|
||||
|
||||
export function pickValidValue(current: string, available: ReadonlyArray<string>): string {
|
||||
if (current && available.includes(current)) {
|
||||
return current;
|
||||
}
|
||||
return available[0] ?? "";
|
||||
}
|
||||
|
||||
export function defaultChapterWordsForLanguage(language: "zh" | "en"): string {
|
||||
return language === "en" ? "2000" : "3000";
|
||||
}
|
||||
|
||||
export function platformOptionsForLanguage(language: "zh" | "en"): ReadonlyArray<PlatformOption> {
|
||||
return language === "en" ? PLATFORMS_EN : PLATFORMS_ZH;
|
||||
}
|
||||
|
||||
export function BookCreate({ nav, theme, t }: { nav: Nav; theme: Theme; t: TFunction }) {
|
||||
const c = useColors(theme);
|
||||
const { data: genreData } = useApi<{ genres: ReadonlyArray<GenreInfo> }>("/genres");
|
||||
@@ -40,7 +60,8 @@ export function BookCreate({ nav, theme, t }: { nav: Nav; theme: Theme; t: TFunc
|
||||
const [title, setTitle] = useState("");
|
||||
const [genre, setGenre] = useState("");
|
||||
const [platform, setPlatform] = useState("");
|
||||
const [chapterWords, setChapterWords] = useState(projectLang === "en" ? "2000" : "3000");
|
||||
const [chapterWords, setChapterWords] = useState(defaultChapterWordsForLanguage(projectLang));
|
||||
const [chapterWordsTouched, setChapterWordsTouched] = useState(false);
|
||||
const [targetChapters, setTargetChapters] = useState("200");
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -48,15 +69,23 @@ export function BookCreate({ nav, theme, t }: { nav: Nav; theme: Theme; t: TFunc
|
||||
// Filter genres by project language + custom genres (always show)
|
||||
const allGenres = genreData?.genres ?? [];
|
||||
const genres = allGenres.filter((g) => g.language === projectLang || g.source === "project");
|
||||
const platforms = projectLang === "en" ? PLATFORMS_EN : PLATFORMS_ZH;
|
||||
const platforms = platformOptionsForLanguage(projectLang);
|
||||
const genreSignature = genres.map((g) => g.id).join("|");
|
||||
const platformSignature = platforms.map((p) => `${p.value}:${p.label}`).join("|");
|
||||
|
||||
// Auto-select first matching genre
|
||||
if (genres.length > 0 && !genre) {
|
||||
setGenre(genres[0]!.id);
|
||||
}
|
||||
if (platforms.length > 0 && !platform) {
|
||||
setPlatform(platforms[0]!.value);
|
||||
}
|
||||
useEffect(() => {
|
||||
setGenre((current) => pickValidValue(current, genres.map((g) => g.id)));
|
||||
}, [genreSignature]);
|
||||
|
||||
useEffect(() => {
|
||||
setPlatform((current) => pickValidValue(current, platforms.map((p) => p.value)));
|
||||
}, [platformSignature]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chapterWordsTouched) {
|
||||
setChapterWords(defaultChapterWordsForLanguage(projectLang));
|
||||
}
|
||||
}, [projectLang, chapterWordsTouched]);
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!title.trim()) {
|
||||
@@ -139,15 +168,15 @@ export function BookCreate({ nav, theme, t }: { nav: Nav; theme: Theme; t: TFunc
|
||||
{/* Platform — filtered by language */}
|
||||
<div>
|
||||
<label className="block text-sm text-muted-foreground mb-2">
|
||||
{projectLang === "zh" ? "平台" : "Platform"}
|
||||
{t("create.platform")}
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
{platforms.map((p, i) => (
|
||||
{platforms.map((p) => (
|
||||
<button
|
||||
key={`${p.value}-${i}`}
|
||||
key={p.value}
|
||||
onClick={() => setPlatform(p.value)}
|
||||
className={`px-3 py-2 rounded-md text-sm transition-all ${
|
||||
platform === p.value && platforms.indexOf(platforms.find((x) => x.value === platform && x.label === p.label)!) === i
|
||||
platform === p.value
|
||||
? "bg-primary/15 text-primary border border-primary/30"
|
||||
: "bg-secondary text-secondary-foreground border border-transparent hover:border-border"
|
||||
}`}
|
||||
@@ -165,7 +194,10 @@ export function BookCreate({ nav, theme, t }: { nav: Nav; theme: Theme; t: TFunc
|
||||
<input
|
||||
type="number"
|
||||
value={chapterWords}
|
||||
onChange={(e) => setChapterWords(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setChapterWordsTouched(true);
|
||||
setChapterWords(e.target.value);
|
||||
}}
|
||||
className={`w-full ${c.input} rounded-md px-4 py-3 focus:outline-none`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
33
packages/studio/src/pages/page-state.test.ts
Normal file
33
packages/studio/src/pages/page-state.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
defaultChapterWordsForLanguage,
|
||||
platformOptionsForLanguage,
|
||||
pickValidValue,
|
||||
} from "./BookCreate";
|
||||
|
||||
describe("pickValidValue", () => {
|
||||
it("keeps the current value when it is still available", () => {
|
||||
expect(pickValidValue("mystery", ["mystery", "romance"])).toBe("mystery");
|
||||
});
|
||||
|
||||
it("falls back to the first available value when current is blank or invalid", () => {
|
||||
expect(pickValidValue("", ["mystery", "romance"])).toBe("mystery");
|
||||
expect(pickValidValue("invalid", ["mystery", "romance"])).toBe("mystery");
|
||||
expect(pickValidValue("", [])).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("defaultChapterWordsForLanguage", () => {
|
||||
it("uses 3000 for chinese projects and 2000 for english projects", () => {
|
||||
expect(defaultChapterWordsForLanguage("zh")).toBe("3000");
|
||||
expect(defaultChapterWordsForLanguage("en")).toBe("2000");
|
||||
});
|
||||
});
|
||||
|
||||
describe("platformOptionsForLanguage", () => {
|
||||
it("uses stable, unique values for english platform choices", () => {
|
||||
const values = platformOptionsForLanguage("en").map((option) => option.value);
|
||||
expect(new Set(values).size).toBe(values.length);
|
||||
expect(values).toEqual(["royal-road", "kindle-unlimited", "scribble-hub", "other"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user