diff --git a/package-lock.json b/package-lock.json index 3c385c5..6c39c94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@tailwindcss/vite": "^4.2.1", "@tauri-apps/api": "^2", "@tauri-apps/plugin-opener": "^2", + "marked": "^17.0.4", "react": "^19.1.0", "react-dom": "^19.1.0", "tailwindcss": "^4.2.1", @@ -19,6 +20,7 @@ }, "devDependencies": { "@tauri-apps/cli": "^2", + "@types/marked": "^5.0.2", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", @@ -1924,6 +1926,13 @@ "@types/unist": "*" } }, + "node_modules/@types/marked": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz", + "integrity": "sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -2727,6 +2736,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/marked": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz", + "integrity": "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/mdast-util-to-hast": { "version": "13.2.1", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", diff --git a/package.json b/package.json index 8a36f7c..5157886 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@tailwindcss/vite": "^4.2.1", "@tauri-apps/api": "^2", "@tauri-apps/plugin-opener": "^2", + "marked": "^17.0.4", "react": "^19.1.0", "react-dom": "^19.1.0", "tailwindcss": "^4.2.1", @@ -21,6 +22,7 @@ }, "devDependencies": { "@tauri-apps/cli": "^2", + "@types/marked": "^5.0.2", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", diff --git a/src/App.tsx b/src/App.tsx index dc4db69..d35b9fc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import { RelaysView } from "./components/shared/RelaysView"; import { SettingsView } from "./components/shared/SettingsView"; import { ProfileView } from "./components/profile/ProfileView"; import { ThreadView } from "./components/thread/ThreadView"; +import { ArticleEditor } from "./components/article/ArticleEditor"; import { useUIStore } from "./stores/ui"; function App() { @@ -18,6 +19,7 @@ function App() { {currentView === "settings" && } {currentView === "profile" && } {currentView === "thread" && } + {currentView === "article-editor" && } ); diff --git a/src/components/article/ArticleEditor.tsx b/src/components/article/ArticleEditor.tsx new file mode 100644 index 0000000..873350e --- /dev/null +++ b/src/components/article/ArticleEditor.tsx @@ -0,0 +1,191 @@ +import { useState, useEffect } from "react"; +import { marked } from "marked"; +import { publishArticle } from "../../lib/nostr"; +import { useUIStore } from "../../stores/ui"; + +const DRAFT_KEY = "wrystr_article_draft"; + +function loadDraft() { + try { return JSON.parse(localStorage.getItem(DRAFT_KEY) || "null"); } + catch { return null; } +} + +function saveDraft(data: object) { + localStorage.setItem(DRAFT_KEY, JSON.stringify(data)); +} + +function clearDraft() { + localStorage.removeItem(DRAFT_KEY); +} + +export function ArticleEditor() { + const { goBack } = useUIStore(); + const draft = loadDraft(); + + const [title, setTitle] = useState(draft?.title || ""); + const [content, setContent] = useState(draft?.content || ""); + const [summary, setSummary] = useState(draft?.summary || ""); + const [image, setImage] = useState(draft?.image || ""); + const [tags, setTags] = useState(draft?.tags || ""); + const [mode, setMode] = useState<"write" | "preview">("write"); + const [showMeta, setShowMeta] = useState(false); + const [publishing, setPublishing] = useState(false); + const [error, setError] = useState(null); + const [published, setPublished] = useState(false); + + // Auto-save draft + useEffect(() => { + const t = setTimeout(() => { + saveDraft({ title, content, summary, image, tags }); + }, 1000); + return () => clearTimeout(t); + }, [title, content, summary, image, tags]); + + const renderedHtml = marked(content || "*Nothing to preview yet.*") as string; + const wordCount = content.trim() ? content.trim().split(/\s+/).length : 0; + const canPublish = title.trim().length > 0 && content.trim().length > 0; + + const handlePublish = async () => { + if (!canPublish || publishing) return; + setPublishing(true); + setError(null); + try { + await publishArticle({ + title: title.trim(), + content: content.trim(), + summary: summary.trim() || undefined, + image: image.trim() || undefined, + tags: tags.split(",").map((t) => t.trim()).filter(Boolean), + }); + clearDraft(); + setPublished(true); + setTimeout(goBack, 1500); + } catch (err) { + setError(`Failed to publish: ${err}`); + } finally { + setPublishing(false); + } + }; + + return ( +
+ {/* Header */} +
+
+ + {wordCount > 0 ? `${wordCount} words` : "New article"} + {draft && !published && ( + · draft saved + )} +
+ +
+ {/* Write / Preview toggle */} +
+ + +
+ + + + +
+
+ + {/* Meta panel */} + {showMeta && ( +
+
+ +