diff --git a/.vscode/settings.json b/.vscode/settings.json index 2606af3..93e8743 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "cSpell.words": ["tauri"] + "cSpell.words": ["Homelab", "Insertable", "tauri"] } diff --git a/app/globals.css b/app/globals.css index a0cda2a..b8a1768 100644 --- a/app/globals.css +++ b/app/globals.css @@ -127,4 +127,13 @@ html { @apply font-sans; } + h1 { + @apply text-4xl font-bold; + } + h2 { + @apply text-3xl font-bold; + } + h3 { + @apply text-2xl font-bold; + } } diff --git a/app/write/edit/page.tsx b/app/write/edit/page.tsx new file mode 100644 index 0000000..fcd7ea8 --- /dev/null +++ b/app/write/edit/page.tsx @@ -0,0 +1,173 @@ +"use client"; + +import { useSearchParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import { invoke } from "@tauri-apps/api/core"; +import Link from "next/link"; +import { HomelabApp, Blog } from "@/types"; + +export default function WritePage() { + const params = useSearchParams(); + + const blogId = params.get("id") || null; + + if (!blogId) { + return ( +
+ + No document ID provided. + +
+ ); + } + + const [blog, setBlog] = useState(null); + const [linkedApps, setLinkedApps] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!blogId) return; + + const fetchBlogAndApps = async () => { + try { + setLoading(true); + const currentBlog = (await invoke("get_blog", { id: blogId })) as + | Blog + | undefined; + + if (!currentBlog) { + setError("Blog post not found."); + return; + } + + setBlog(currentBlog); + + // 2. Fetch the metadata for any bound/linked apps if they exist + if (currentBlog.reference && currentBlog.reference.length > 0) { + const apps = (await invoke("get_apps_by_id", { + ids: currentBlog.reference, + })) as HomelabApp[]; + setLinkedApps(apps); + } + } catch (err) { + console.error("Failed to load blog page data:", err); + setError("An error occurred while loading the document."); + } finally { + setLoading(false); + } + }; + + fetchBlogAndApps(); + }, [blogId]); + + if (loading) { + return ( +
+ + Loading document... + +
+ ); + } + + if (error || !blog) { + return ( +
+

+ {error || "Something went wrong."} +

+ + Go Back Home + +
+ ); + } + + return ( +
+ {/* Top Header Controls */} +
+
+

+ {blog.title || "Untitled Document"} +

+
+ + + + + + Edit Document + +
+ + {/* Main Reading Workspace Layout */} +
+ {/* Left Side: Styled Canvas Viewport */} +
+ {/* Using your exact editor style class 'prose max-w-none' to ensure uniform markdown formatting */} +
+
+ + {/* Right Side: Associated Connected Homelab Apps */} + {linkedApps.length > 0 && ( +
+
+

+ Linked Assets +

+

+ Infrastructure context bound to this document. +

+
+ +
+ {linkedApps.map((app) => ( +
+
+ {app.name} + +
+
+ {app.address} +
+ {app.description && ( +
+ {app.description} +
+ )} +
+ ))} +
+
+ )} +
+
+ ); +} diff --git a/app/write/new/page.tsx b/app/write/new/page.tsx new file mode 100644 index 0000000..59bf313 --- /dev/null +++ b/app/write/new/page.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { useEffect, useState, Suspense } from "react"; +import { useSearchParams } from "next/navigation"; +import { invoke } from "@tauri-apps/api/core"; +import TextEditor from "@/components/text-editor"; +import { HomelabApp, Blog } from "@/types"; + +function WritePageContent() { + const searchParams = useSearchParams(); + const appQuery = searchParams.get("app"); + + const [initialAppIds, setInitialAppIds] = useState([]); + const [loading, setLoading] = useState(true); + + // Track the document ID to allow updating instead of duplicating + const [documentId, setDocumentId] = useState(null); + + useEffect(() => { + const checkQueryParam = async () => { + if (!appQuery) { + setLoading(false); + return; + } + + try { + const apps = (await invoke("get_app")) as HomelabApp[]; + + const matchedApp = apps.find( + (app) => + app.id === appQuery || + app.name.toLowerCase().trim() === appQuery.toLowerCase().trim(), + ); + + if (matchedApp) { + setInitialAppIds([matchedApp.id]); + } + } catch (error) { + console.error( + "Failed to fetch apps for query parameter auto-link:", + error, + ); + } finally { + setLoading(false); + } + }; + + checkQueryParam(); + }, [appQuery]); + + // Updated onSave signature matching what TextEditor emits + const onSave = async ( + content: string, + linkedAppIds: string[], + title: string, + ) => { + try { + const savedBlog = await invoke("save_blog", { + id: documentId, + title: title || "Untitled Document", + content: content, + linkedAppIds: linkedAppIds, + }); + + // Update state with the backend-generated/returned ID + if (savedBlog && savedBlog.id) { + setDocumentId(savedBlog.id); + } + } catch (error) { + console.error("Failed to save document:", error); + } + }; + + if (loading) { + return ( +
Loading workspace...
+ ); + } + + return ( + + ); +} + +export default function WritePage() { + return ( +
+ + Loading router state... +
+ } + > + + + + ); +} diff --git a/app/write/page.tsx b/app/write/page.tsx index 76e80ee..b889f31 100644 --- a/app/write/page.tsx +++ b/app/write/page.tsx @@ -1,3 +1,31 @@ -export default function WritePage() { - return

Write Page

; +"use client"; + +import { invoke } from "@tauri-apps/api/core"; +import { Blog } from "@/types"; +import { useEffect, useState } from "react"; +import BlogCard from "@/components/blog-card"; + +export default function ViewBlogPage() { + const [blogs, setBlogs] = useState([]); + + useEffect(() => { + const fetchBlogs = async () => { + return await invoke("get_blogs"); + }; + + fetchBlogs().then((blogs) => { + console.log("Blogs:", blogs); + setBlogs(blogs as Blog[]); + }); + }, []); + + return ( +
+
+ {blogs.map((blog) => ( + + ))} +
+
+ ); } diff --git a/bun.lock b/bun.lock index 5c1ac48..3db5d64 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,17 @@ "@tanstack/react-query": "^5.101.2", "@tauri-apps/api": "^2", "@tauri-apps/plugin-opener": "^2", + "@tiptap/extension-highlight": "^3.27.1", + "@tiptap/extension-mention": "^3.27.1", + "@tiptap/extension-placeholder": "^3.27.1", + "@tiptap/extension-text-align": "^3.27.1", + "@tiptap/extension-typography": "^3.27.1", + "@tiptap/extensions": "^3.27.1", + "@tiptap/markdown": "^3.27.1", + "@tiptap/pm": "^3.27.1", + "@tiptap/react": "^3.27.1", + "@tiptap/starter-kit": "^3.27.1", + "@tiptap/suggestion": "^3.27.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.21.0", @@ -20,6 +31,8 @@ "shadcn": "^4.12.0", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", + "tippy.js": "^6.3.7", + "tiptap-markdown": "^0.9.0", "tw-animate-css": "^1.4.0", }, "devDependencies": { @@ -217,6 +230,8 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@popperjs/core": ["@popperjs/core@2.11.8", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="], + "@radix-ui/number": ["@radix-ui/number@1.1.2", "", {}, "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.4", "", {}, "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ=="], @@ -405,14 +420,92 @@ "@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.4", "", { "dependencies": { "@tauri-apps/api": "^2.11.0" } }, "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ=="], + "@tiptap/core": ["@tiptap/core@3.27.1", "", { "peerDependencies": { "@tiptap/pm": "3.27.1" } }, "sha512-rV6Qn4wmC6BxfF+4mu6bqGWj9vA4oXXhsrpXaJL2uhjxeHAGofjwcHof2X84VYzeyXgdlsGmqKie4TAppVXZUQ=="], + + "@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-VMF7xJx6qEGiX6DTKNiL31NLqypOcd/4sNjFSe8rb41PwejBJh/nOqVIbBvWkiT6NMGFLxMhj7zJ8/zPo1hXeg=="], + + "@tiptap/extension-bold": ["@tiptap/extension-bold@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-TlC5bsS+pqETTrlz4CZz9RO/cKBYtELGIxwtKeivUn3eNfnOxQbbu4WDsiwIfzRFyd0OMnKl6BPM2KnYEehoEQ=="], + + "@tiptap/extension-bubble-menu": ["@tiptap/extension-bubble-menu@3.27.1", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-j/j8Qp9Z5nViade2m7zjrO/CYH/Ca80Qj7aqo0eUaei6FZQ5izlF9o4XQU5EFMAutV6mwynsPUp8FVo5sCuYfw=="], + + "@tiptap/extension-bullet-list": ["@tiptap/extension-bullet-list@3.27.1", "", { "peerDependencies": { "@tiptap/extension-list": "3.27.1" } }, "sha512-faCUHnRP47o9Zh9VZZX6EX/569udw9Vopm2PgEKPWuKLE2qaS5WBuUVU0iItdJmKUqaWiOZkpoW4jvnDmj0dfg=="], + + "@tiptap/extension-code": ["@tiptap/extension-code@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-epOUpFfEmBzjvnqvjv2qHX7NAuLo5dlOGV690lWu+sAYMjibuJBeVvAiKPyFCfRCCTUxdbDB3jbaOA1yEcEJ7w=="], + + "@tiptap/extension-code-block": ["@tiptap/extension-code-block@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-pHlzmZx2OlHfyQ0yRlT5UL4mGokz947DthZuYefN1OleVqOkHpWBG+2JQwqoNq6bmzMne92zbH32rhcJUEYSjA=="], + + "@tiptap/extension-document": ["@tiptap/extension-document@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-8FbBTkfnRP4iVaoj+2h3iWa+H0eGDD3yTyVCwrmue/sQTkqUNUoSuAZa3GDG4Sd41xdPwTJxl9nUWGgM1qDCnw=="], + + "@tiptap/extension-dropcursor": ["@tiptap/extension-dropcursor@3.27.1", "", { "peerDependencies": { "@tiptap/extensions": "3.27.1" } }, "sha512-blFf9x9RG0Qr7P3FoAH/033ffa+mMLZn34trVs8Vi0Ppk6FmJAg5HpYFOtmYoeREdNDJ5rHJKV7SoACbOHgskQ=="], + + "@tiptap/extension-floating-menu": ["@tiptap/extension-floating-menu@3.27.1", "", { "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-BmJF1VqB7dSJkgAalrpVFj88WLhxKjcWPuWHOqf2ITrUU2832BhKLXKmxjWUy1gqV8PfNNVWtGfIERy7I0y0+Q=="], + + "@tiptap/extension-gapcursor": ["@tiptap/extension-gapcursor@3.27.1", "", { "peerDependencies": { "@tiptap/extensions": "3.27.1" } }, "sha512-QoezN0wdvXIwLQ4ee2ccWDaX3RG0lzgQpIMpMz55oPDhpUVax1+19ApsS53LkcktpS4EbnPL4xO4DaJk0Vp7PQ=="], + + "@tiptap/extension-hard-break": ["@tiptap/extension-hard-break@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-iv/m9hzl6jfSj9Q8UEjAxONvCoUDaP7M9SRCPx3PaLNxA230TTD6RE0Ye4zFJ8ze7ZVoJJMAqg9Qpq1iYg2JOQ=="], + + "@tiptap/extension-heading": ["@tiptap/extension-heading@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-SrC4l1kEIyv9ZXFaI/8LQqU2MyMmjczw7XXsWUQOTN4YXv0JyVgMNR3cI/wz0d2xsTfBdZ1N85Tdng+Ga1t0Sg=="], + + "@tiptap/extension-highlight": ["@tiptap/extension-highlight@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-IeMIUKbW4oyRkgn92BPYQ0fifkbTwVigKwa107nGDHMokz+pHQPFHy8xG3U7gtUOra/8OUo57bFgNGuP0TaH4A=="], + + "@tiptap/extension-horizontal-rule": ["@tiptap/extension-horizontal-rule@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-QlKE7qn5qMnIGVGhXQlvYedvLtNJ9z0dmit5w8vPb8tKzW4Spk6M7N2kruprrDA8GBwHfeR5wmF+njfUm34qxg=="], + + "@tiptap/extension-italic": ["@tiptap/extension-italic@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-jGGeyn9uRUnNjSTHpbqhiGsp6KaYTSbV09jDXPJI9cDwfV9hpugLvpaCZd0BMBbhU1B1W6kOfX0BE15qX/HQfA=="], + + "@tiptap/extension-link": ["@tiptap/extension-link@3.27.1", "", { "dependencies": { "linkifyjs": "^4.3.3" }, "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-/2jBfsxBZUDGJmpZifqRQPz7f1E5qpS1BckTZ39TADzUJX+feKy7RJ3DtQ02+8y6SSMzvP9loGVjrk6zEMTk4g=="], + + "@tiptap/extension-list": ["@tiptap/extension-list@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-c2Upru7lj0/ZV/Ibww6cNz6sUS8m6Dp/9uygFhYcZOd3X8M0xBIEk42c6m6SQehkPziVA8QOgNJz7sMqsbz1OQ=="], + + "@tiptap/extension-list-item": ["@tiptap/extension-list-item@3.27.1", "", { "peerDependencies": { "@tiptap/extension-list": "3.27.1" } }, "sha512-zwRl01ETfCkWUvtvK5fw9bXtAajMPkvlkE3Cq6JvH3LF7XXJwDtNj5Tj7exacMpCaSZmlNc43vFb2rAYnrnwMA=="], + + "@tiptap/extension-list-keymap": ["@tiptap/extension-list-keymap@3.27.1", "", { "peerDependencies": { "@tiptap/extension-list": "3.27.1" } }, "sha512-OIMZNlzPSO8WRd4ic73Fxckzl4N1tesjjLL2XApaNA/uMpO0LoF6WSRPAWv+Z24Wp92ARRJAnRP7iZoI5+Jxig=="], + + "@tiptap/extension-mention": ["@tiptap/extension-mention@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1", "@tiptap/suggestion": "3.27.1" } }, "sha512-QfaKdl8PET01JvEFrIjMcOC1iYrBm2tsZEn07J1AaCqClW9iWcg/nCAdkdFhVir1fC54SLuaui43xslBawQRFg=="], + + "@tiptap/extension-ordered-list": ["@tiptap/extension-ordered-list@3.27.1", "", { "peerDependencies": { "@tiptap/extension-list": "3.27.1" } }, "sha512-GYrKqD//9nHJ2r80uXqbDMzRnFpGzbaEQRTSGaO/SH7DvXWFMow8evkOdjQ7PCQO07jNjJo75+A85Jwu3Ov3AA=="], + + "@tiptap/extension-paragraph": ["@tiptap/extension-paragraph@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-7K7eo1gruOgAsnbK+GCV23AUVUI0cL1bTig8HaPneoFMVbig7vddk8jNLKBWO8TXVbG7TuHdnDN4F98vdtwh5Q=="], + + "@tiptap/extension-placeholder": ["@tiptap/extension-placeholder@3.27.1", "", { "peerDependencies": { "@tiptap/extensions": "3.27.1" } }, "sha512-lhcNDcczQ75yJOSywCHb58Hmtg1aPL/2TdYmeLPxVrP048D7rRs133sfONcgyyw0AvhnfmPOkHLTv3QtKSowhw=="], + + "@tiptap/extension-strike": ["@tiptap/extension-strike@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-Y3DW1jlSlCNCyMGHP3+3qBNNPS83wuFz4RTYGjZtvRRTCRh7apZme9XRWMq1rN5mJ2Cr7fKocA2/5Bs13KgN6Q=="], + + "@tiptap/extension-text": ["@tiptap/extension-text@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-6ZwaZwSrDh+KFFv6V1J79oO37yPs7y1bFxvk1/9Ih2rn3Xr5AWz+eMS+n8RpH3djBVVAQpdIAeYQgcn+VCSsTg=="], + + "@tiptap/extension-text-align": ["@tiptap/extension-text-align@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-EXawuJBO55wd8WcTbHTMoPhv0CGQxza4yCCPB5Hqz4ZPQwahIr3ej+8yp/kimIl0xokabwZ0/Fu8STQ4AkZv5g=="], + + "@tiptap/extension-typography": ["@tiptap/extension-typography@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-hkZyiiv45XQGizdEYxJn86g8yKJxG45kkTztg0m6qjjcdDzjhUAcwVPTZ4ecg3hxwy/XNY7gEBOMGI7iqJl2sw=="], + + "@tiptap/extension-underline": ["@tiptap/extension-underline@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1" } }, "sha512-N889J4nXN/TPfVt8uF9N1A0SY82E90zwc1y26lqOcw6KWNLmQrlhMh/9OD4ikLDbekmFpOBq/UicpHf/6S8hbQ=="], + + "@tiptap/extensions": ["@tiptap/extensions@3.27.1", "", { "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-1Tdx9faw8k0/83V6X+xCDVhV8yElGt95JxeW3YMkKQJI56QdlPz0xOdJPlMiSGJKinPyVier+x9LJD/YZUZIaw=="], + + "@tiptap/markdown": ["@tiptap/markdown@3.27.1", "", { "dependencies": { "marked": "^17.0.1" }, "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-4m2LZcj/uN0uLlGnXr3i7sfdxbQNNmeMn4wFyFrP2xpshcKPL5WujUAcqT2rgKfaDjKQ8gIK/GpgSQKuRENFwg=="], + + "@tiptap/pm": ["@tiptap/pm@3.27.1", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.3", "prosemirror-model": "^1.25.7", "prosemirror-schema-list": "^1.5.0", "prosemirror-state": "^1.4.4", "prosemirror-tables": "^1.8.0", "prosemirror-transform": "^1.12.0", "prosemirror-view": "^1.41.8" } }, "sha512-Ffjx+vimmBU7zH/KrpXzJid3+pziCe/VL2aexSTP63cyQwKQ65LkFkCKaIsSpFdQQuakVZBGWjCA5RoBV852pw=="], + + "@tiptap/react": ["@tiptap/react@3.27.1", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "fast-equals": "^5.3.3", "use-sync-external-store": "^1.4.0" }, "optionalDependencies": { "@tiptap/extension-bubble-menu": "^3.27.1", "@tiptap/extension-floating-menu": "^3.27.1" }, "peerDependencies": { "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-/Wn2fc9zMtX08MXYScDFsm4wJ8lzfhfPEdbtls7WCDlbtrop48PWlkHDBBJrywARfAQTB2mFs9KiFy9yrQm5Lg=="], + + "@tiptap/starter-kit": ["@tiptap/starter-kit@3.27.1", "", { "dependencies": { "@tiptap/core": "^3.27.1", "@tiptap/extension-blockquote": "^3.27.1", "@tiptap/extension-bold": "^3.27.1", "@tiptap/extension-bullet-list": "^3.27.1", "@tiptap/extension-code": "^3.27.1", "@tiptap/extension-code-block": "^3.27.1", "@tiptap/extension-document": "^3.27.1", "@tiptap/extension-dropcursor": "^3.27.1", "@tiptap/extension-gapcursor": "^3.27.1", "@tiptap/extension-hard-break": "^3.27.1", "@tiptap/extension-heading": "^3.27.1", "@tiptap/extension-horizontal-rule": "^3.27.1", "@tiptap/extension-italic": "^3.27.1", "@tiptap/extension-link": "^3.27.1", "@tiptap/extension-list": "^3.27.1", "@tiptap/extension-list-item": "^3.27.1", "@tiptap/extension-list-keymap": "^3.27.1", "@tiptap/extension-ordered-list": "^3.27.1", "@tiptap/extension-paragraph": "^3.27.1", "@tiptap/extension-strike": "^3.27.1", "@tiptap/extension-text": "^3.27.1", "@tiptap/extension-underline": "^3.27.1", "@tiptap/extensions": "^3.27.1", "@tiptap/pm": "^3.27.1" } }, "sha512-vfxRsqW8rCc0k4pzo0ilU3wobVi2wqVj88VZI2SlgZlNnUAkrDGDIAph7CTa9k9fshV+O1ivpEgPC5yC046jow=="], + + "@tiptap/suggestion": ["@tiptap/suggestion@3.27.1", "", { "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "3.27.1", "@tiptap/pm": "3.27.1" } }, "sha512-GNBPRav+lAfXzqmmUAS6ylRAn3G8JfsP6XosjoORxJIQJLx1ktDqwp6tm1Vgz9aGIM2TrBxLS1uBbI1Gb2/1VA=="], + "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], + "@types/linkify-it": ["@types/linkify-it@3.0.5", "", {}, "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw=="], + + "@types/markdown-it": ["@types/markdown-it@13.0.9", "", { "dependencies": { "@types/linkify-it": "^3", "@types/mdurl": "^1" } }, "sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw=="], + + "@types/mdurl": ["@types/mdurl@1.0.5", "", {}, "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA=="], + "@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="], "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], + "@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], @@ -537,6 +630,8 @@ "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], @@ -567,6 +662,8 @@ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], @@ -717,6 +814,10 @@ "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + "linkify-it": ["linkify-it@5.0.1", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg=="], + + "linkifyjs": ["linkifyjs@4.3.3", "", {}, "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg=="], + "locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], @@ -727,8 +828,16 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "markdown-it": ["markdown-it@14.2.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ=="], + + "markdown-it-task-lists": ["markdown-it-task-lists@2.1.1", "", {}, "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA=="], + + "marked": ["marked@17.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], @@ -787,6 +896,8 @@ "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], + "orderedmap": ["orderedmap@2.1.1", "", {}, "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="], + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], @@ -827,8 +938,38 @@ "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + "prosemirror-changeset": ["prosemirror-changeset@2.4.1", "", { "dependencies": { "prosemirror-transform": "^1.0.0" } }, "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw=="], + + "prosemirror-commands": ["prosemirror-commands@1.7.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.10.2" } }, "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w=="], + + "prosemirror-dropcursor": ["prosemirror-dropcursor@1.8.2", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0", "prosemirror-view": "^1.1.0" } }, "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw=="], + + "prosemirror-gapcursor": ["prosemirror-gapcursor@1.4.1", "", { "dependencies": { "prosemirror-keymap": "^1.0.0", "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-view": "^1.0.0" } }, "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw=="], + + "prosemirror-history": ["prosemirror-history@1.5.0", "", { "dependencies": { "prosemirror-state": "^1.2.2", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.31.0", "rope-sequence": "^1.3.0" } }, "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg=="], + + "prosemirror-inputrules": ["prosemirror-inputrules@1.5.1", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw=="], + + "prosemirror-keymap": ["prosemirror-keymap@1.2.3", "", { "dependencies": { "prosemirror-state": "^1.0.0", "w3c-keyname": "^2.2.0" } }, "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw=="], + + "prosemirror-markdown": ["prosemirror-markdown@1.13.4", "", { "dependencies": { "@types/markdown-it": "^14.0.0", "markdown-it": "^14.0.0", "prosemirror-model": "^1.25.0" } }, "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw=="], + + "prosemirror-model": ["prosemirror-model@1.25.9", "", { "dependencies": { "orderedmap": "^2.0.0" } }, "sha512-pRTklkDDMMRopyoAcrr9wV/8g/RYgrLHBuJAb5hlEuYZRdm5yqmPjWId83fpBwPpSFqEdja0H7Dfd7z1X/npcA=="], + + "prosemirror-schema-list": ["prosemirror-schema-list@1.5.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.7.3" } }, "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q=="], + + "prosemirror-state": ["prosemirror-state@1.4.4", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.27.0" } }, "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw=="], + + "prosemirror-tables": ["prosemirror-tables@1.8.5", "", { "dependencies": { "prosemirror-keymap": "^1.2.3", "prosemirror-model": "^1.25.4", "prosemirror-state": "^1.4.4", "prosemirror-transform": "^1.10.5", "prosemirror-view": "^1.41.4" } }, "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw=="], + + "prosemirror-transform": ["prosemirror-transform@1.12.0", "", { "dependencies": { "prosemirror-model": "^1.21.0" } }, "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w=="], + + "prosemirror-view": ["prosemirror-view@1.41.9", "", { "dependencies": { "prosemirror-model": "^1.25.8", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-clTunTX+eaLbr87L1V1QPheRlEQJyTlL3gXe9x3jQIk3rL0RVWxviDGz8tFaydwIVm+hKhYCyr+R/zBtWr9s6A=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], + "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -859,6 +1000,8 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], @@ -929,6 +1072,10 @@ "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + "tippy.js": ["tippy.js@6.3.7", "", { "dependencies": { "@popperjs/core": "^2.9.0" } }, "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ=="], + + "tiptap-markdown": ["tiptap-markdown@0.9.0", "", { "dependencies": { "@types/markdown-it": "^13.0.7", "markdown-it": "^14.1.0", "markdown-it-task-lists": "^2.1.1", "prosemirror-markdown": "^1.11.1" }, "peerDependencies": { "@tiptap/core": "^3.0.1" } }, "sha512-dKLQ9iiuGNgrlGVjrNauF/UBzWu4LYOx5pkD0jNkmQt/GOwfCJsBuzZTsf1jZ204ANHOm572mZ9PYvGh1S7tpQ=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], @@ -945,6 +1092,8 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], + "undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], @@ -961,12 +1110,16 @@ "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="], "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], + "which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], @@ -1033,6 +1186,8 @@ "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + "prosemirror-markdown/@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="], + "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], @@ -1056,5 +1211,9 @@ "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "prosemirror-markdown/@types/markdown-it/@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="], + + "prosemirror-markdown/@types/markdown-it/@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="], } } diff --git a/components/action-menu.tsx b/components/action-menu.tsx index 3c9935b..f93a29c 100644 --- a/components/action-menu.tsx +++ b/components/action-menu.tsx @@ -11,6 +11,7 @@ import { } from "@/components/ui/tooltip"; import DeleteDialog from "./delete-dialog"; import { useState } from "react"; +import { useRouter } from "next/navigation"; type Props = { id: string; @@ -22,6 +23,8 @@ export default function ActionMenu({ id }: Props) { const [open, setOpen] = useState(false); + const router = useRouter(); + return ( <> @@ -69,7 +72,9 @@ export default function ActionMenu({ id }: Props) { + + + + +
+
+ +
+ +
+
+

+ Document Assets +

+

+ Click apps to bind/unbind them to this entire document. +

+
+ +
+ {availableApps.map((app) => { + const isLinked = linkedAppIds.includes(app.id); + return ( + + ); + })} +
+
+
+ + ); +} diff --git a/components/text-editor/index.tsx b/components/text-editor/index.tsx new file mode 100644 index 0000000..16b9933 --- /dev/null +++ b/components/text-editor/index.tsx @@ -0,0 +1,195 @@ +"use client"; + +import { useEditor, EditorContent } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import Highlight from "@tiptap/extension-highlight"; +import TextAlign from "@tiptap/extension-text-align"; +import { Markdown } from "@tiptap/markdown"; +import { HomelabApp } from "@/types"; +import { invoke } from "@tauri-apps/api/core"; +import { useEffect, useState } from "react"; +import Menubar from "./menu-bar"; + +interface TextEditorProps { + content: string; + initialTitle?: string; + onSave: (content: string, linkedAppIds: string[], title: string) => void; + initialLinkedAppIds?: string[]; +} + +const getApps = async (): Promise => { + return (await invoke("get_app")) as HomelabApp[]; +}; + +export default function TextEditor({ + content, + initialTitle = "", + onSave, + initialLinkedAppIds = [], +}: TextEditorProps) { + const [availableApps, setAvailableApps] = useState([]); + const [linkedAppIds, setLinkedAppIds] = + useState(initialLinkedAppIds); + // Title control state + const [title, setTitle] = useState(initialTitle); + // Save control + // Will be saved on completed handleSave, error if func fails, and saving if the func is in progress + const [isSaved, setIsSaved] = useState<"saved" | "error" | "saving">( + "saving", + ); + + useEffect(() => { + if (initialLinkedAppIds.length > 0) { + setLinkedAppIds(initialLinkedAppIds); + } + }, [initialLinkedAppIds]); + + useEffect(() => { + const fetchApps = async () => { + const apps = await getApps(); + setAvailableApps(apps); + }; + fetchApps(); + }, []); + + const editor = useEditor({ + extensions: [ + StarterKit.configure({ + bulletList: { HTMLAttributes: { class: "list-disc ml-3" } }, + orderedList: { HTMLAttributes: { class: "list-decimal ml-3" } }, + }), + Highlight.configure({ HTMLAttributes: { class: "bg-yellow-200" } }), + TextAlign.configure({ types: ["heading", "paragraph"] }), + Markdown, + ], + immediatelyRender: false, + content, + editorProps: { + attributes: { + class: "prose max-w-none focus:outline-none min-h-[300px]", + }, + }, + }); + + const toggleAppLink = (appId: string) => { + setLinkedAppIds((prev) => + prev.includes(appId) + ? prev.filter((id) => id !== appId) + : [...prev, appId], + ); + }; + + const handleSave = () => { + setIsSaved("saving"); + try { + if (!editor) return; + const htmlOutput = editor.getHTML(); + // Dispatch text body layout, assets collection array, and document title string + onSave(htmlOutput, linkedAppIds, title); + setIsSaved("saved"); + } catch (error) { + console.error("Error saving document:", error); + setIsSaved("error"); + } + }; + + // create debounced save, every 15 seconds + useEffect(() => { + const interval = setInterval(() => { + handleSave(); + }, 15000); + + return () => clearInterval(interval); + }, [editor, linkedAppIds, title]); + + return ( +
+ {/* Top action bar updated with live Title Input */} +
+ setTitle(e.target.value)} + placeholder="Untitled Document..." + className="bg-transparent text-zinc-800 font-semibold text-base placeholder-zinc-300 focus:outline-none flex-1 py-1" + /> +
+ {isSaved === "saving" && ( + Saving... + )} + {isSaved === "saved" && ( + Saved + )} + {isSaved === "error" && ( + Error saving + )} + +
+
+ + {/* Editor Controls */} + + + {/* Main split-view workspace */} +
+ {/* Left Side: Text Canvas */} +
+ +
+ + {/* Right Side: Document-Wide Reference Panel */} +
+
+

+ Document Assets +

+

+ Click apps to bind/unbind them to this entire document. +

+
+ +
+ {availableApps.map((app) => { + const isLinked = linkedAppIds.includes(app.id); + return ( + + ); + })} +
+
+
+
+ ); +} diff --git a/components/text-editor/menu-bar.tsx b/components/text-editor/menu-bar.tsx new file mode 100644 index 0000000..9226da4 --- /dev/null +++ b/components/text-editor/menu-bar.tsx @@ -0,0 +1,122 @@ +import { Editor, useEditorState } from "@tiptap/react"; +import { + AlignCenter, + AlignLeft, + AlignRight, + Bold, + Heading1, + Heading2, + Heading3, + Highlighter, + Italic, + List, + ListOrdered, +} from "lucide-react"; +import { Toggle } from "../ui/toggle"; + +interface MenubarProps { + editor: Editor | null; +} + +// 1. The main exported component remains safe and handles the null state cleanly +export default function Menubar({ editor }: MenubarProps) { + if (!editor) { + return null; + } + + // Only render the inner menu once the editor is explicitly available + return ; +} + +// 2. The inner component runs safely because 'editor' is guaranteed to be an Editor instance +function MenubarInner({ editor }: { editor: Editor }) { + const editorState = useEditorState({ + editor, // Now safely guaranteed never to be null or undefined + selector: (ctx) => { + return { + isBold: ctx.editor.isActive("bold"), + isItalic: ctx.editor.isActive("italic"), + isStrike: ctx.editor.isActive("strike"), + isHighlight: ctx.editor.isActive("highlight"), + + isAlignLeft: ctx.editor.isActive({ textAlign: "left" }), + isAlignCenter: ctx.editor.isActive({ textAlign: "center" }), + isAlignRight: ctx.editor.isActive({ textAlign: "right" }), + isAlignJustify: ctx.editor.isActive({ textAlign: "justify" }), + + isParagraph: ctx.editor.isActive("paragraph"), + isHeading1: ctx.editor.isActive("heading", { level: 1 }), + isHeading2: ctx.editor.isActive("heading", { level: 2 }), + isHeading3: ctx.editor.isActive("heading", { level: 3 }), + }; + }, + }); + + const options = [ + { + icon: , + onClick: () => editor.chain().focus().toggleHeading({ level: 1 }).run(), + pressed: editorState.isHeading1, + }, + { + icon: , + onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run(), + pressed: editorState.isHeading2, + }, + { + icon: , + onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(), + pressed: editorState.isHeading3, + }, + { + icon: , + onClick: () => editor.chain().focus().toggleBold().run(), + pressed: editorState.isBold, + }, + { + icon: , + onClick: () => editor.chain().focus().toggleItalic().run(), + pressed: editorState.isItalic, + }, + { + icon: , + onClick: () => editor.chain().focus().toggleTextAlign("left").run(), + pressed: editorState.isAlignLeft, + }, + { + icon: , + onClick: () => editor.chain().focus().toggleTextAlign("center").run(), + pressed: editorState.isAlignCenter, + }, + { + icon: , + onClick: () => editor.chain().focus().toggleTextAlign("right").run(), + pressed: editorState.isAlignRight, + }, + { + icon: , + onClick: () => editor.chain().focus().toggleBulletList().run(), + pressed: editor.isActive("bulletList"), + }, + { + icon: , + onClick: () => editor.chain().focus().toggleOrderedList().run(), + pressed: editor.isActive("orderedList"), + }, + { + icon: , + onClick: () => editor.chain().focus().toggleHighlight().run(), + pressed: editorState.isHighlight, + }, + ]; + + return ( +
+ {options.map((option, index) => ( + + {option.icon} + + ))} +
+ ); +} diff --git a/components/ui/toggle.tsx b/components/ui/toggle.tsx new file mode 100644 index 0000000..ea86236 --- /dev/null +++ b/components/ui/toggle.tsx @@ -0,0 +1,47 @@ +"use client"; + +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { Toggle as TogglePrimitive } from "radix-ui"; + +import { cn } from "@/lib/utils"; + +const toggleVariants = cva( + "group/toggle inline-flex items-center justify-center gap-1 rounded-2xl text-sm font-medium whitespace-nowrap transition-colors outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/30 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-transparent", + outline: "border border-input bg-transparent hover:bg-muted", + }, + size: { + default: + "h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + sm: "h-7 min-w-7 px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5", + lg: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +function Toggle({ + className, + variant = "default", + size = "default", + ...props +}: React.ComponentProps & + VariantProps) { + return ( + + ); +} + +export { Toggle, toggleVariants }; diff --git a/package.json b/package.json index 820e724..3bc1e29 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,17 @@ "@tanstack/react-query": "^5.101.2", "@tauri-apps/api": "^2", "@tauri-apps/plugin-opener": "^2", + "@tiptap/extension-highlight": "^3.27.1", + "@tiptap/extension-mention": "^3.27.1", + "@tiptap/extension-placeholder": "^3.27.1", + "@tiptap/extension-text-align": "^3.27.1", + "@tiptap/extension-typography": "^3.27.1", + "@tiptap/extensions": "^3.27.1", + "@tiptap/markdown": "^3.27.1", + "@tiptap/pm": "^3.27.1", + "@tiptap/react": "^3.27.1", + "@tiptap/starter-kit": "^3.27.1", + "@tiptap/suggestion": "^3.27.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.21.0", @@ -26,6 +37,8 @@ "shadcn": "^4.12.0", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", + "tippy.js": "^6.3.7", + "tiptap-markdown": "^0.9.0", "tw-animate-css": "^1.4.0" }, "devDependencies": { diff --git a/src-tauri/src/db/mod.rs b/src-tauri/src/db/mod.rs index f642b96..e8c031c 100644 --- a/src-tauri/src/db/mod.rs +++ b/src-tauri/src/db/mod.rs @@ -53,3 +53,114 @@ pub fn delete_app(conn: &mut SqliteConnection, app_id: &str) -> usize { .execute(conn) .expect("Error deleting app") } + +pub fn create_blog_db( + conn: &mut SqliteConnection, + title: &str, + content: &str, + linked_app_ids: Option<&[String]>, +) -> models::Blog { + use crate::db::schema::blog; + + // json seralize the referenced app ids, store as string + if let Some(ids) = linked_app_ids { + for id in ids { + // Check if the app exists in the database + let app_exists = crate::db::schema::app::dsl::app + .filter(crate::db::schema::app::dsl::id.eq(id)) + .first::(conn) + .optional() + .expect("Error checking if app exists"); + + if app_exists.is_none() { + panic!("App with id {} does not exist", id); + } + } + } + + // if there are linked app ids, serialize them to json, else set to None + let reference: Option = if let Some(ids) = linked_app_ids { + Some(serde_json::to_string(&ids).expect("Failed to convert to JSON")) + } else { + None + }; + + let new_blog = models::Blog { + id: uuid::Uuid::new_v4().to_string(), + title: title.to_string(), + content: content.to_string(), + reference, + }; + + diesel::insert_into(blog::table) + .values(&new_blog) + .execute(conn) + .expect("Error saving new blog"); + + new_blog +} + +// Will be used to update an existing blog post OR create a new one if it doesn't exist +pub fn save_blog( + conn: &mut SqliteConnection, + blog_id: Option<&str>, + title: &str, + content: &str, + linked_app_ids: Option<&[String]>, +) -> models::Blog { + use crate::db::schema::blog; + + // json seralize the referenced app ids, store as string + let reference: Option = if let Some(ids) = linked_app_ids { + Some(serde_json::to_string(&ids).expect("Failed to convert to JSON")) + } else { + None + }; + + if let Some(id) = blog_id { + // Update existing blog + diesel::update(blog::table.filter(blog::id.eq(id))) + .set(( + blog::title.eq(title), + blog::content.eq(content), + blog::reference.eq(reference), + )) + .execute(conn) + .expect("Error updating blog"); + + blog::table + .filter(blog::id.eq(id)) + .first::(conn) + .expect("Error loading updated blog") + } else { + // Create new blog + create_blog_db(conn, title, content, linked_app_ids) + } +} + +pub fn get_all_blogs(conn: &mut SqliteConnection) -> Vec { + use crate::db::schema::blog; + + blog::table + .load::(conn) + .expect("Error loading apps") +} + +pub fn get_apps_by_ids(conn: &mut SqliteConnection, ids: &[String]) -> Vec { + use crate::db::schema::app; + + app::table + .filter(app::id.eq_any(ids)) + .load::(conn) + .expect("Error loading apps by ids") +} + +pub fn get_blog_by_id(conn: &mut SqliteConnection, blog_id: &str) -> Option { + use crate::db::schema::blog; + + blog::table + .filter(blog::id.eq(blog_id)) + .first::(conn) + .optional() + .expect("Error loading blog by id") +} diff --git a/src-tauri/src/db/models.rs b/src-tauri/src/db/models.rs index 5c40e29..d98c80e 100644 --- a/src-tauri/src/db/models.rs +++ b/src-tauri/src/db/models.rs @@ -12,7 +12,7 @@ pub struct App { pub ssh_address: Option, // default to normal address } -#[derive(Queryable, Selectable, Debug, Serialize, Deserialize)] +#[derive(Queryable, Selectable, Debug, Insertable, Serialize, Deserialize)] #[diesel(table_name = crate::db::schema::blog)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct Blog { @@ -22,7 +22,7 @@ pub struct Blog { pub reference: Option, // uuid } -#[derive(Queryable, Selectable, Debug, Serialize, Deserialize)] +#[derive(Queryable, Selectable, Debug, Insertable, Serialize, Deserialize)] #[diesel(table_name = crate::db::schema::identities)] #[diesel(check_for_backend(diesel::sqlite::Sqlite))] pub struct Identity { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7861d6b..a4426f6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -27,14 +27,12 @@ async fn ping_target(address: &str) -> bool { let ip_target: IpAddr = socket_addr.ip(); - // 2. Configure unprivileged ICMP requestor let timeout = Some(Duration::from_secs(2)); let pinger = match IcmpEchoRequestor::new(ip_target, None, None, timeout) { Ok(p) => p, Err(_) => return false, }; - // 3. Send ICMP Echo request asynchronously match pinger.send().await { Ok(reply) => { // Check if the status returned is a valid success response @@ -46,7 +44,7 @@ async fn ping_target(address: &str) -> bool { #[tauri::command] fn new_app(name: &str, address: &str, description: Option<&str>, ssh_address: Option<&str>) -> App { - let mut conn = &mut establish_connection(); + let mut conn = establish_connection(); let app = create_app(&mut conn, name, address, description, ssh_address); println!("{:#?}", app); @@ -55,7 +53,7 @@ fn new_app(name: &str, address: &str, description: Option<&str>, ssh_address: Op #[tauri::command] fn get_app() -> Vec { - let mut conn = &mut establish_connection(); + let mut conn = establish_connection(); let apps = get_apps(&mut conn); println!("{:#?}", apps); @@ -64,8 +62,8 @@ fn get_app() -> Vec { #[tauri::command] fn delete_apps(id: &str) { - let conn = &mut establish_connection(); - delete_app(conn, id); + let mut conn = establish_connection(); + delete_app(&mut conn, id); } #[tauri::command] @@ -76,6 +74,72 @@ async fn ping(address: &str) -> Result { } } +#[derive(Debug, serde::Serialize, serde::Deserialize)] +struct TempBlog { + id: String, + title: String, + content: String, + linked_app_ids: Option>, +} + +#[tauri::command] +fn get_blogs() -> Vec { + // I'll need to decode the linked_app_ids from a json encoded string to a Vec. + let mut conn = establish_connection(); + let encoded_blogs = db::get_all_blogs(&mut conn); + let mut decoded_blogs: Vec = Vec::new(); + encoded_blogs.iter().for_each(|blog| { + let linked_app_ids: Option> = match &blog.reference { + Some(ids) => serde_json::from_str(ids).ok(), + None => None, + }; + decoded_blogs.push(TempBlog { + id: blog.id.clone(), + title: blog.title.clone(), + content: blog.content.clone(), + linked_app_ids, + }); + }); + + decoded_blogs +} + +#[tauri::command] +fn save_blog( + id: Option<&str>, + title: &str, + content: &str, + linked_app_ids: Option>, +) -> db::models::Blog { + let mut conn = establish_connection(); + db::save_blog(&mut conn, id, title, content, linked_app_ids.as_deref()) +} + +#[tauri::command] +fn get_apps_by_id(ids: Vec) -> Vec { + let mut conn = establish_connection(); + db::get_apps_by_ids(&mut conn, &ids) +} + +#[tauri::command] +fn get_blog(id: &str) -> Option { + let mut conn = establish_connection(); + let blog = db::get_blog_by_id(&mut conn, id); + + blog.map(|b| { + let linked_app_ids: Option> = match &b.reference { + Some(ids) => serde_json::from_str(ids).ok(), + None => None, + }; + TempBlog { + id: b.id.clone(), + title: b.title.clone(), + content: b.content.clone(), + linked_app_ids, + } + }) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() @@ -84,7 +148,11 @@ pub fn run() { new_app, get_app, delete_apps, - ping + ping, + get_blogs, + save_blog, + get_apps_by_id, + get_blog ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000..ded567e --- /dev/null +++ b/types.d.ts @@ -0,0 +1,20 @@ +export interface HomelabApp { + id: string; // uuid + name: string; + description: string | null; + address: string; + ssh_address: string | null; +} + +export interface Blog { + id: string; + title: string; + content: string; + reference: String[]; // list of app ids +} + +// Defines the custom attributes Tiptap will store in the DOM/JSON schema +export interface AppMentionAttributes { + id: string; + label: string; +}