diff --git a/src/app/[lang]/tools/toolSeoData.js b/src/app/[lang]/tools/toolSeoData.js index da87287..23e597a 100644 --- a/src/app/[lang]/tools/toolSeoData.js +++ b/src/app/[lang]/tools/toolSeoData.js @@ -92,6 +92,10 @@ export const TOOL_METADATA_DATES = { publishedDate: "2026-05-05T08:00:00.000Z", updatedDate: "2026-05-05T08:00:00.000Z", }, + wordcount: { + publishedDate: "2026-05-05T09:00:00.000Z", + updatedDate: "2026-05-05T09:00:00.000Z", + }, yamljson: { publishedDate: "2026-05-05T07:00:00.000Z", updatedDate: "2026-05-05T07:00:00.000Z", diff --git a/src/app/[lang]/tools/wordcount/ClientContent.js b/src/app/[lang]/tools/wordcount/ClientContent.js new file mode 100644 index 0000000..d3ca4fe --- /dev/null +++ b/src/app/[lang]/tools/wordcount/ClientContent.js @@ -0,0 +1,13 @@ +"use client"; + +import dynamic from "next/dynamic"; +import ToolContentLoading from "@/app/components/ToolContentLoading"; + +const WordCountContent = dynamic(() => import("./content"), { + ssr: false, + loading: () => , +}); + +export default function ClientContent() { + return ; +} diff --git a/src/app/[lang]/tools/wordcount/__tests__/logic.test.js b/src/app/[lang]/tools/wordcount/__tests__/logic.test.js new file mode 100644 index 0000000..45fa573 --- /dev/null +++ b/src/app/[lang]/tools/wordcount/__tests__/logic.test.js @@ -0,0 +1,24 @@ +import { analyzeText, countChineseCharacters, countEnglishWords, countParagraphs, getLimitStatus } from "../logic"; + +describe("wordcount logic", () => { + test("counts Chinese characters and English words separately", () => { + const text = "你好 world, this is GPT-5.\n第二段 text."; + expect(countChineseCharacters(text)).toBe(5); + expect(countEnglishWords(text)).toBe(5); + }); + + test("analyzes text structure", () => { + const stats = analyzeText("第一句。Second sentence!\n\nNew paragraph."); + expect(stats.paragraphs).toBe(2); + expect(stats.sentences).toBe(3); + expect(stats.charsNoSpaces).toBeLessThan(stats.chars); + expect(stats.readingMinutes).toBe(1); + }); + + test("counts paragraphs and social limits", () => { + expect(countParagraphs("a\n\nb\n\n\nc")).toBe(3); + const status = getLimitStatus(analyzeText("hello"), { metric: "chars", limit: 3 }); + expect(status.exceeded).toBe(true); + expect(status.remaining).toBe(-2); + }); +}); diff --git a/src/app/[lang]/tools/wordcount/content.js b/src/app/[lang]/tools/wordcount/content.js new file mode 100644 index 0000000..2b6fa32 --- /dev/null +++ b/src/app/[lang]/tools/wordcount/content.js @@ -0,0 +1,157 @@ +"use client"; + +import { useMemo, useRef, useState } from "react"; +import { saveAs } from "file-saver"; +import { useI18n } from "@/app/i18n/client"; +import { SOCIAL_LIMITS, analyzeText, getLimitStatus, makeStatsReport } from "./logic"; + +const EXAMPLES = { + zh: "这是一段用于字数统计的中文文案。它可以统计中文字符、英文 words、标点、段落和句子。\n\n如果你正在写小红书标题、公众号摘要、SEO 标题或产品介绍,可以把文本粘贴到这里,实时查看长度是否超出限制。", + en: "This is a sample paragraph for word counting. It counts English words, characters, sentences, paragraphs, reading time, and common social media limits.\n\nPaste a blog intro, SEO title, product copy, or social post here to check whether the text fits your target channel.", +}; + +function StatCard({ label, value, hint }) { + return ( +
+

{label}

+

{value}

+ {hint &&

{hint}

} +
+ ); +} + +export default function WordCountContent() { + const { t, lang } = useI18n(); + const fileInputRef = useRef(null); + const [text, setText] = useState(""); + const [copyStatus, setCopyStatus] = useState(""); + const stats = useMemo(() => analyzeText(text), [text]); + const hasText = text.trim().length > 0; + + const loadExample = () => { + setText(EXAMPLES[lang] || EXAMPLES.en); + setCopyStatus(""); + }; + + const clearText = () => { + setText(""); + setCopyStatus(""); + if (fileInputRef.current) fileInputRef.current.value = ""; + }; + + const copyReport = async () => { + await navigator.clipboard.writeText(makeStatsReport(stats)); + setCopyStatus(t("wordcount_copied")); + }; + + const downloadReport = () => { + saveAs(new Blob([makeStatsReport(stats)], { type: "application/json;charset=utf-8" }), "word-count-report.json"); + }; + + const uploadTextFile = async (event) => { + const file = event.target.files?.[0]; + if (!file) return; + setText(await file.text()); + setCopyStatus(""); + }; + + return ( +
+
+
+
+

{t("wordcount_workspace_title")}

+

{t("wordcount_workspace_hint")}

+
+
+ + + +
+ +
+ +
+