|
|
@@ -13,26 +13,50 @@ interface GenreInfo {
|
|
|
readonly id: string;
|
|
|
readonly name: string;
|
|
|
readonly source: "project" | "builtin";
|
|
|
+ readonly language: "zh" | "en";
|
|
|
}
|
|
|
|
|
|
+const PLATFORMS_ZH = [
|
|
|
+ { 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" },
|
|
|
+ { value: "other", label: "Other" },
|
|
|
+];
|
|
|
+
|
|
|
export function BookCreate({ nav, theme, t }: { nav: Nav; theme: Theme; t: TFunction }) {
|
|
|
const c = useColors(theme);
|
|
|
const { data: genreData } = useApi<{ genres: ReadonlyArray<GenreInfo> }>("/genres");
|
|
|
+ const { data: project } = useApi<{ language: string }>("/project");
|
|
|
+
|
|
|
+ const projectLang = (project?.language ?? "zh") as "zh" | "en";
|
|
|
|
|
|
const [title, setTitle] = useState("");
|
|
|
const [genre, setGenre] = useState("");
|
|
|
- const [language, setLanguage] = useState<"zh" | "en">("zh");
|
|
|
- const [chapterWords, setChapterWords] = useState("3000");
|
|
|
+ const [platform, setPlatform] = useState("");
|
|
|
+ const [chapterWords, setChapterWords] = useState(projectLang === "en" ? "2000" : "3000");
|
|
|
const [targetChapters, setTargetChapters] = useState("200");
|
|
|
const [creating, setCreating] = useState(false);
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
- const genres = genreData?.genres ?? [];
|
|
|
+ // 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;
|
|
|
|
|
|
- // Auto-select first genre when data loads
|
|
|
+ // Auto-select first matching genre
|
|
|
if (genres.length > 0 && !genre) {
|
|
|
setGenre(genres[0]!.id);
|
|
|
}
|
|
|
+ if (platforms.length > 0 && !platform) {
|
|
|
+ setPlatform(platforms[0]!.value);
|
|
|
+ }
|
|
|
|
|
|
const handleCreate = async () => {
|
|
|
if (!title.trim()) {
|
|
|
@@ -49,7 +73,8 @@ export function BookCreate({ nav, theme, t }: { nav: Nav; theme: Theme; t: TFunc
|
|
|
const result = await postApi<{ bookId: string }>("/books/create", {
|
|
|
title: title.trim(),
|
|
|
genre,
|
|
|
- language,
|
|
|
+ language: projectLang,
|
|
|
+ platform,
|
|
|
chapterWordCount: parseInt(chapterWords, 10),
|
|
|
targetChapters: parseInt(targetChapters, 10),
|
|
|
});
|
|
|
@@ -62,83 +87,95 @@ export function BookCreate({ nav, theme, t }: { nav: Nav; theme: Theme; t: TFunc
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
- <div className="max-w-lg mx-auto space-y-6">
|
|
|
- <div className={`flex items-center gap-2 text-sm ${c.muted}`}>
|
|
|
+ <div className="max-w-lg mx-auto space-y-8">
|
|
|
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
|
<button onClick={nav.toDashboard} className={c.link}>{t("bread.books")}</button>
|
|
|
- <span>/</span>
|
|
|
- <span className={c.subtle}>{t("bread.newBook")}</span>
|
|
|
+ <span className="text-border">/</span>
|
|
|
+ <span>{t("bread.newBook")}</span>
|
|
|
</div>
|
|
|
|
|
|
- <h1 className="text-2xl font-semibold">{t("create.title")}</h1>
|
|
|
+ <h1 className="font-serif text-3xl">{t("create.title")}</h1>
|
|
|
|
|
|
{error && (
|
|
|
- <div className={`border ${c.error} rounded-lg px-4 py-3 text-sm`}>
|
|
|
+ <div className={`border ${c.error} rounded-md px-4 py-3`}>
|
|
|
{error}
|
|
|
</div>
|
|
|
)}
|
|
|
|
|
|
- <div className="space-y-4">
|
|
|
+ <div className="space-y-5">
|
|
|
+ {/* Title */}
|
|
|
<div>
|
|
|
- <label className={`block text-sm ${c.subtle} mb-1`}>{t("create.bookTitle")}</label>
|
|
|
+ <label className="block text-sm text-muted-foreground mb-2">{t("create.bookTitle")}</label>
|
|
|
<input
|
|
|
type="text"
|
|
|
value={title}
|
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
|
- className={`w-full ${c.input} rounded-md px-3 py-2 focus:outline-none`}
|
|
|
+ className={`w-full ${c.input} rounded-md px-4 py-3 focus:outline-none text-base`}
|
|
|
placeholder={t("create.placeholder")}
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
+ {/* Genre — filtered by language */}
|
|
|
<div>
|
|
|
- <label className={`block text-sm ${c.subtle} mb-1`}>{t("create.language")}</label>
|
|
|
- <div className="flex gap-2">
|
|
|
- <button
|
|
|
- onClick={() => setLanguage("zh")}
|
|
|
- className={`px-4 py-2 rounded-md text-sm ${language === "zh" ? c.btnPrimary : c.btnSecondary}`}
|
|
|
- >
|
|
|
- Chinese
|
|
|
- </button>
|
|
|
- <button
|
|
|
- onClick={() => setLanguage("en")}
|
|
|
- className={`px-4 py-2 rounded-md text-sm ${language === "en" ? c.btnPrimary : c.btnSecondary}`}
|
|
|
- >
|
|
|
- English
|
|
|
- </button>
|
|
|
+ <label className="block text-sm text-muted-foreground mb-2">{t("create.genre")}</label>
|
|
|
+ <div className="grid grid-cols-3 gap-2">
|
|
|
+ {genres.map((g) => (
|
|
|
+ <button
|
|
|
+ key={g.id}
|
|
|
+ onClick={() => setGenre(g.id)}
|
|
|
+ className={`px-3 py-2.5 rounded-md text-sm text-left transition-all ${
|
|
|
+ genre === g.id
|
|
|
+ ? "bg-primary/15 text-primary border border-primary/30 font-medium"
|
|
|
+ : "bg-secondary text-secondary-foreground border border-transparent hover:border-border"
|
|
|
+ }`}
|
|
|
+ >
|
|
|
+ {g.name}
|
|
|
+ {g.source === "project" && <span className="text-xs text-muted-foreground ml-1">✦</span>}
|
|
|
+ </button>
|
|
|
+ ))}
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ {/* Platform — filtered by language */}
|
|
|
<div>
|
|
|
- <label className={`block text-sm ${c.subtle} mb-1`}>{t("create.genre")}</label>
|
|
|
- <select
|
|
|
- value={genre}
|
|
|
- onChange={(e) => setGenre(e.target.value)}
|
|
|
- className={`w-full ${c.input} rounded-md px-3 py-2.5 focus:outline-none appearance-none cursor-pointer`}
|
|
|
- >
|
|
|
- {genres.map((g) => (
|
|
|
- <option key={g.id} value={g.id}>
|
|
|
- {g.name} ({g.id}){g.source === "project" ? " [custom]" : ""}
|
|
|
- </option>
|
|
|
+ <label className="block text-sm text-muted-foreground mb-2">
|
|
|
+ {projectLang === "zh" ? "平台" : "Platform"}
|
|
|
+ </label>
|
|
|
+ <div className="flex gap-2">
|
|
|
+ {platforms.map((p, i) => (
|
|
|
+ <button
|
|
|
+ key={`${p.value}-${i}`}
|
|
|
+ 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
|
|
|
+ ? "bg-primary/15 text-primary border border-primary/30"
|
|
|
+ : "bg-secondary text-secondary-foreground border border-transparent hover:border-border"
|
|
|
+ }`}
|
|
|
+ >
|
|
|
+ {p.label}
|
|
|
+ </button>
|
|
|
))}
|
|
|
- </select>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
+ {/* Word count + chapters */}
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
<div>
|
|
|
- <label className={`block text-sm ${c.subtle} mb-1`}>{t("create.wordsPerChapter")}</label>
|
|
|
+ <label className="block text-sm text-muted-foreground mb-2">{t("create.wordsPerChapter")}</label>
|
|
|
<input
|
|
|
type="number"
|
|
|
value={chapterWords}
|
|
|
onChange={(e) => setChapterWords(e.target.value)}
|
|
|
- className={`w-full ${c.input} rounded-md px-3 py-2 focus:outline-none`}
|
|
|
+ className={`w-full ${c.input} rounded-md px-4 py-3 focus:outline-none`}
|
|
|
/>
|
|
|
</div>
|
|
|
<div>
|
|
|
- <label className={`block text-sm ${c.subtle} mb-1`}>{t("create.targetChapters")}</label>
|
|
|
+ <label className="block text-sm text-muted-foreground mb-2">{t("create.targetChapters")}</label>
|
|
|
<input
|
|
|
type="number"
|
|
|
value={targetChapters}
|
|
|
onChange={(e) => setTargetChapters(e.target.value)}
|
|
|
- className={`w-full ${c.input} rounded-md px-3 py-2 focus:outline-none`}
|
|
|
+ className={`w-full ${c.input} rounded-md px-4 py-3 focus:outline-none`}
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -147,7 +184,7 @@ export function BookCreate({ nav, theme, t }: { nav: Nav; theme: Theme; t: TFunc
|
|
|
<button
|
|
|
onClick={handleCreate}
|
|
|
disabled={creating || !title.trim()}
|
|
|
- className={`w-full px-4 py-2.5 ${c.btnPrimary} rounded-md transition-colors disabled:opacity-50 font-medium`}
|
|
|
+ className={`w-full px-4 py-3 ${c.btnPrimary} rounded-md disabled:opacity-50 font-medium text-base`}
|
|
|
>
|
|
|
{creating ? t("create.creating") : t("create.submit")}
|
|
|
</button>
|