WIP: blogging. you have a markdown editor with linking capabilities, along with the ability to view, edit, and create other blogs. #1
Vendored
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"cSpell.words": ["tauri"]
|
||||
"cSpell.words": ["Homelab", "Insertable", "tauri"]
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex items-center justify-center min-h-100">
|
||||
<span className="text-sm text-zinc-400 animate-pulse">
|
||||
No document ID provided.
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const [blog, setBlog] = useState<Blog | null>(null);
|
||||
const [linkedApps, setLinkedApps] = useState<HomelabApp[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<div className="flex items-center justify-center min-h-100">
|
||||
<span className="text-sm text-zinc-400 animate-pulse">
|
||||
Loading document...
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !blog) {
|
||||
return (
|
||||
<div className="p-6 text-center max-w-xl mx-auto mt-12 border border-red-200 bg-red-50/50 rounded-lg">
|
||||
<p className="text-sm text-red-600 font-medium">
|
||||
{error || "Something went wrong."}
|
||||
</p>
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-block mt-4 text-xs font-semibold text-zinc-600 hover:text-zinc-800 underline"
|
||||
>
|
||||
Go Back Home
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto px-4 py-8 w-full flex flex-col gap-6">
|
||||
{/* Top Header Controls */}
|
||||
<div className="flex justify-between items-center pb-4 border-b border-zinc-100 gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-zinc-900 tracking-tight">
|
||||
{blog.title || "Untitled Document"}
|
||||
</h1>
|
||||
</div>
|
||||
<Link
|
||||
href={`/write/edit/${blog.id}`}
|
||||
className="px-4 py-1.5 bg-zinc-900 hover:bg-zinc-800 text-white font-medium text-sm rounded-md transition-colors shrink-0 flex items-center gap-1.5 shadow-sm"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M12 20h9" />
|
||||
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" />
|
||||
</svg>
|
||||
Edit Document
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Main Reading Workspace Layout */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8 items-start">
|
||||
{/* Left Side: Styled Canvas Viewport */}
|
||||
<div className="md:col-span-3 border border-zinc-100 rounded-xl p-6 md:p-8 bg-white shadow-sm min-h-[400px]">
|
||||
{/* Using your exact editor style class 'prose max-w-none' to ensure uniform markdown formatting */}
|
||||
<article
|
||||
className="prose max-w-none text-zinc-800 prose-headings:text-zinc-900 prose-a:text-emerald-600 prose-strong:text-zinc-900"
|
||||
dangerouslySetInnerHTML={{ __html: blog.content }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Side: Associated Connected Homelab Apps */}
|
||||
{linkedApps.length > 0 && (
|
||||
<div className="md:col-span-1 bg-zinc-50 border border-zinc-200/60 rounded-xl p-4 flex flex-col gap-3 sticky top-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-zinc-700 text-xs uppercase tracking-wider">
|
||||
Linked Assets
|
||||
</h3>
|
||||
<p className="text-[11px] text-zinc-400 mt-0.5">
|
||||
Infrastructure context bound to this document.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 mt-1">
|
||||
{linkedApps.map((app) => (
|
||||
<div
|
||||
key={app.id}
|
||||
className="w-full bg-white border border-zinc-200 p-3 rounded-lg shadow-2vsm"
|
||||
>
|
||||
<div className="font-semibold text-xs text-zinc-800 flex items-center justify-between">
|
||||
<span className="truncate">{app.name}</span>
|
||||
<span
|
||||
className="h-1.5 w-1.5 rounded-full bg-emerald-500 shrink-0 ml-1"
|
||||
title="Bound asset"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-[10px] text-zinc-400 font-mono truncate mt-0.5 selection:bg-zinc-100">
|
||||
{app.address}
|
||||
</div>
|
||||
{app.description && (
|
||||
<div className="text-[11px] text-zinc-500 mt-1.5 line-clamp-2 border-t border-zinc-50 pt-1.5">
|
||||
{app.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Track the document ID to allow updating instead of duplicating
|
||||
const [documentId, setDocumentId] = useState<string | null>(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<Blog>("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 (
|
||||
<div className="p-4 text-sm text-zinc-400">Loading workspace...</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TextEditor
|
||||
content=""
|
||||
onSave={onSave}
|
||||
initialLinkedAppIds={initialAppIds}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WritePage() {
|
||||
return (
|
||||
<div className="flex flex-col p-2">
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="p-4 text-sm text-zinc-400">
|
||||
Loading router state...
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<WritePageContent />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+30
-2
@@ -1,3 +1,31 @@
|
||||
export default function WritePage() {
|
||||
return <h1>Write Page</h1>;
|
||||
"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<Blog[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchBlogs = async () => {
|
||||
return await invoke("get_blogs");
|
||||
};
|
||||
|
||||
fetchBlogs().then((blogs) => {
|
||||
console.log("Blogs:", blogs);
|
||||
setBlogs(blogs as Blog[]);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col p-2">
|
||||
<div className="grid xs:grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 m-4">
|
||||
{blogs.map((blog) => (
|
||||
<BlogCard key={blog.id} blog={blog} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<TooltipProvider delayDuration={150}>
|
||||
@@ -69,7 +72,9 @@ export default function ActionMenu({ id }: Props) {
|
||||
<button
|
||||
className={buttonClass}
|
||||
onClick={() => {
|
||||
// Blog
|
||||
let link = `/write/new?app=${id}`;
|
||||
console.log(link);
|
||||
router.push(link);
|
||||
}}
|
||||
>
|
||||
<Pencil className="h-5 w-5" />
|
||||
|
||||
@@ -90,11 +90,7 @@ export default function AppCard({ app }: Props) {
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl font-bold">{app.name}</h3>
|
||||
<a
|
||||
className={`${hovered && "text-blue-600"} transition-colors duration-200`}
|
||||
target="_blank"
|
||||
href={`http://${app.address}`}
|
||||
>
|
||||
<a target="_blank" href={`http://${app.address}`}>
|
||||
{app.address}
|
||||
</a>
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { Blog, HomelabApp } from "@/types";
|
||||
import { useEffect, useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface BlogCardProps {
|
||||
blog: Blog & { linked_app_ids?: string[] };
|
||||
}
|
||||
|
||||
export default function BlogCard({ blog }: BlogCardProps) {
|
||||
const [referencedApps, setReferencedApps] = useState<HomelabApp[]>([]);
|
||||
const appIds = blog.linked_app_ids;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (appIds && appIds.length > 0) {
|
||||
const fetchReferences = async () => {
|
||||
try {
|
||||
const apps = await invoke<HomelabApp[]>("get_apps_by_id", {
|
||||
ids: appIds,
|
||||
});
|
||||
setReferencedApps(apps);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch referenced apps:", err);
|
||||
}
|
||||
};
|
||||
fetchReferences();
|
||||
}
|
||||
}, [appIds]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group flex flex-col justify-between p-6 border border-slate-200/80 rounded-2xl bg-gradient-to-b from-white to-slate-50/50 dark:from-slate-900 dark:to-slate-950 text-card-foreground shadow-sm hover:shadow-xl hover:-translate-y-1 hover:border-emerald-600/40 transition-all duration-300 h-64 relative overflow-hidden"
|
||||
onClick={() => router.push(`/write/edit?id=${blog.id}`)}
|
||||
>
|
||||
{/* Decorative background glow that triggers on hover */}
|
||||
<div className="absolute -right-10 -top-10 w-24 h-24 bg-emerald-500/10 rounded-full blur-2xl group-hover:bg-emerald-500/20 transition-all duration-500" />
|
||||
|
||||
<div>
|
||||
{/* Category / Meta indicator */}
|
||||
<span className="text-[10px] font-bold tracking-wider uppercase text-emerald-700/80 dark:text-emerald-400 mb-1 block">
|
||||
Homelab Log
|
||||
</span>
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="text-xl font-bold tracking-tight line-clamp-1 mb-2 text-slate-800 dark:text-slate-100 group-hover:text-emerald-700 dark:group-hover:text-emerald-400 transition-colors duration-200">
|
||||
{blog.title}
|
||||
</h3>
|
||||
|
||||
{/* Truncated Tiptap HTML Content */}
|
||||
<div
|
||||
className="prose prose-sm dark:prose-invert line-clamp-3 text-slate-500 dark:text-slate-400 overflow-hidden max-w-none leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: blog.content }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer / References */}
|
||||
<div className="mt-4 pt-4 border-t border-slate-100 dark:border-slate-800/80 flex flex-wrap gap-2 items-center overflow-hidden max-h-12">
|
||||
{!appIds || appIds.length === 0 ? (
|
||||
<span className="text-xs text-slate-400 italic">
|
||||
No linked applications
|
||||
</span>
|
||||
) : (
|
||||
referencedApps.map((app) => (
|
||||
<span
|
||||
key={app.id}
|
||||
title={app.address}
|
||||
className="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-semibold bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 border border-emerald-500/20 hover:bg-emerald-500/20 transition-colors"
|
||||
>
|
||||
{/* App Avatar Placeholder (Uses first letter of app name) */}
|
||||
<span className="w-4 h-4 rounded bg-emerald-700 text-white dark:bg-emerald-500 dark:text-slate-950 font-bold text-[9px] flex items-center justify-center mr-1.5 shadow-sm uppercase shrink-0">
|
||||
{app.name.charAt(0)}
|
||||
</span>
|
||||
{app.name}
|
||||
</span>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/* Custom component node pill styling inside the editor wrapper */
|
||||
.homelab-app-reference-pill {
|
||||
background-color: rgba(
|
||||
16,
|
||||
185,
|
||||
129,
|
||||
0.1
|
||||
); /* Emerald tint matching your UI accents */
|
||||
color: #059669;
|
||||
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.1rem 0.4rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Visually highlight the whole card pill block if a user hits delete or selects it */
|
||||
.ProseMirror-selectednode .homelab-app-reference-pill {
|
||||
outline: 2px solid #10b981;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
"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;
|
||||
onSave: (content: string, linkedAppIds: string[]) => void;
|
||||
initialLinkedAppIds?: string[]; // Added prop
|
||||
}
|
||||
|
||||
const getApps = async (): Promise<HomelabApp[]> => {
|
||||
return (await invoke("get_app")) as HomelabApp[];
|
||||
};
|
||||
|
||||
export default function TextEditor({
|
||||
content,
|
||||
onSave,
|
||||
initialLinkedAppIds = [],
|
||||
}: TextEditorProps) {
|
||||
const [availableApps, setAvailableApps] = useState<HomelabApp[]>([]);
|
||||
// Seed state instantly from the prop if it exists
|
||||
const [linkedAppIds, setLinkedAppIds] =
|
||||
useState<string[]>(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: content || "<p>Start typing your documentation notes here...</p>",
|
||||
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 = () => {
|
||||
if (!editor) return;
|
||||
const htmlOutput = editor.getHTML();
|
||||
onSave(htmlOutput, linkedAppIds);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<div className="flex justify-between items-center pb-2 border-b border-zinc-100">
|
||||
<span className="text-sm text-zinc-400 font-medium">Draft Session</span>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="px-4 py-1.5 bg-emerald-600 hover:bg-emerald-700 text-white font-medium text-sm rounded-md transition-colors"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Menubar editor={editor} />
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 items-start">
|
||||
<div className="md:col-span-3 border border-zinc-200 rounded-lg p-4 bg-white shadow-sm min-h-100">
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-1 bg-zinc-50 border border-zinc-200 rounded-lg p-4 flex flex-col gap-3">
|
||||
<div>
|
||||
<h3 className="font-semibold text-zinc-700 text-sm">
|
||||
Document Assets
|
||||
</h3>
|
||||
<p className="text-xs text-zinc-400 mt-0.5">
|
||||
Click apps to bind/unbind them to this entire document.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 mt-2">
|
||||
{availableApps.map((app) => {
|
||||
const isLinked = linkedAppIds.includes(app.id);
|
||||
return (
|
||||
<button
|
||||
key={app.id}
|
||||
type="button"
|
||||
onClick={() => toggleAppLink(app.id)}
|
||||
className={`w-full text-left p-3 border rounded-md transition relative overflow-hidden ${
|
||||
isLinked
|
||||
? "border-emerald-500 bg-emerald-50/30"
|
||||
: "bg-white border-zinc-200 hover:border-zinc-300"
|
||||
}`}
|
||||
>
|
||||
{isLinked && (
|
||||
<div className="absolute top-0 right-0 h-0 w-0 border-t-[16px] border-t-emerald-500 border-l-[16px] border-l-transparent" />
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`font-medium text-xs ${isLinked ? "text-emerald-700" : "text-zinc-800"}`}
|
||||
>
|
||||
{app.name}
|
||||
</div>
|
||||
<div className="text-[10px] text-zinc-400 font-mono truncate mt-0.5">
|
||||
{app.id}
|
||||
</div>
|
||||
{app.description && (
|
||||
<div className="text-[11px] text-zinc-500 mt-1 line-clamp-1">
|
||||
{app.description}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<HomelabApp[]> => {
|
||||
return (await invoke("get_app")) as HomelabApp[];
|
||||
};
|
||||
|
||||
export default function TextEditor({
|
||||
content,
|
||||
initialTitle = "",
|
||||
onSave,
|
||||
initialLinkedAppIds = [],
|
||||
}: TextEditorProps) {
|
||||
const [availableApps, setAvailableApps] = useState<HomelabApp[]>([]);
|
||||
const [linkedAppIds, setLinkedAppIds] =
|
||||
useState<string[]>(initialLinkedAppIds);
|
||||
// Title control state
|
||||
const [title, setTitle] = useState<string>(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 (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
{/* Top action bar updated with live Title Input */}
|
||||
<div className="flex justify-between items-center pb-2 border-b border-zinc-100 gap-4">
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
{isSaved === "saving" && (
|
||||
<span className="text-xs text-zinc-400">Saving...</span>
|
||||
)}
|
||||
{isSaved === "saved" && (
|
||||
<span className="text-xs text-emerald-600">Saved</span>
|
||||
)}
|
||||
{isSaved === "error" && (
|
||||
<span className="text-xs text-red-600">Error saving</span>
|
||||
)}
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="px-4 py-1.5 bg-emerald-600 hover:bg-emerald-700 text-white font-medium text-sm rounded-md transition-colors shrink-0"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editor Controls */}
|
||||
<Menubar editor={editor} />
|
||||
|
||||
{/* Main split-view workspace */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 items-start">
|
||||
{/* Left Side: Text Canvas */}
|
||||
<div className="md:col-span-3 border border-zinc-200 rounded-lg p-4 bg-white shadow-sm min-h-100">
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
|
||||
{/* Right Side: Document-Wide Reference Panel */}
|
||||
<div className="md:col-span-1 bg-zinc-50 border border-zinc-200 rounded-lg p-4 flex flex-col gap-3">
|
||||
<div>
|
||||
<h3 className="font-semibold text-zinc-700 text-sm">
|
||||
Document Assets
|
||||
</h3>
|
||||
<p className="text-xs text-zinc-400 mt-0.5">
|
||||
Click apps to bind/unbind them to this entire document.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 mt-2">
|
||||
{availableApps.map((app) => {
|
||||
const isLinked = linkedAppIds.includes(app.id);
|
||||
return (
|
||||
<button
|
||||
key={app.id}
|
||||
type="button"
|
||||
onClick={() => toggleAppLink(app.id)}
|
||||
className={`w-full text-left p-3 border rounded-md transition relative overflow-hidden ${
|
||||
isLinked
|
||||
? "border-emerald-500 bg-emerald-50/30"
|
||||
: "bg-white border-zinc-200 hover:border-zinc-300"
|
||||
}`}
|
||||
>
|
||||
{isLinked && (
|
||||
<div className="absolute top-0 right-0 h-0 w-0 border-t-16 border-t-emerald-500 border-l-16 border-l-transparent" />
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`font-medium text-xs ${isLinked ? "text-emerald-700" : "text-zinc-800"}`}
|
||||
>
|
||||
{app.name}
|
||||
</div>
|
||||
<div className="text-[10px] text-zinc-400 font-mono truncate mt-0.5">
|
||||
{app.id}
|
||||
</div>
|
||||
{app.description && (
|
||||
<div className="text-[11px] text-zinc-500 mt-1 line-clamp-1">
|
||||
{app.description}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 <MenubarInner editor={editor} />;
|
||||
}
|
||||
|
||||
// 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: <Heading1 className="size-4" />,
|
||||
onClick: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
||||
pressed: editorState.isHeading1,
|
||||
},
|
||||
{
|
||||
icon: <Heading2 className="size-4" />,
|
||||
onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
||||
pressed: editorState.isHeading2,
|
||||
},
|
||||
{
|
||||
icon: <Heading3 className="size-4" />,
|
||||
onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
||||
pressed: editorState.isHeading3,
|
||||
},
|
||||
{
|
||||
icon: <Bold className="size-4" />,
|
||||
onClick: () => editor.chain().focus().toggleBold().run(),
|
||||
pressed: editorState.isBold,
|
||||
},
|
||||
{
|
||||
icon: <Italic className="size-4" />,
|
||||
onClick: () => editor.chain().focus().toggleItalic().run(),
|
||||
pressed: editorState.isItalic,
|
||||
},
|
||||
{
|
||||
icon: <AlignLeft className="size-4" />,
|
||||
onClick: () => editor.chain().focus().toggleTextAlign("left").run(),
|
||||
pressed: editorState.isAlignLeft,
|
||||
},
|
||||
{
|
||||
icon: <AlignCenter className="size-4" />,
|
||||
onClick: () => editor.chain().focus().toggleTextAlign("center").run(),
|
||||
pressed: editorState.isAlignCenter,
|
||||
},
|
||||
{
|
||||
icon: <AlignRight className="size-4" />,
|
||||
onClick: () => editor.chain().focus().toggleTextAlign("right").run(),
|
||||
pressed: editorState.isAlignRight,
|
||||
},
|
||||
{
|
||||
icon: <List className="size-4" />,
|
||||
onClick: () => editor.chain().focus().toggleBulletList().run(),
|
||||
pressed: editor.isActive("bulletList"),
|
||||
},
|
||||
{
|
||||
icon: <ListOrdered className="size-4" />,
|
||||
onClick: () => editor.chain().focus().toggleOrderedList().run(),
|
||||
pressed: editor.isActive("orderedList"),
|
||||
},
|
||||
{
|
||||
icon: <Highlighter className="size-4" />,
|
||||
onClick: () => editor.chain().focus().toggleHighlight().run(),
|
||||
pressed: editorState.isHighlight,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="border rounded-md p-1 mb-1 bg-slate-50 space-x-2 z-50 flex items-center">
|
||||
{options.map((option, index) => (
|
||||
<Toggle key={index} pressed={option.pressed} onClick={option.onClick}>
|
||||
{option.icon}
|
||||
</Toggle>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<TogglePrimitive.Root
|
||||
data-slot="toggle"
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Toggle, toggleVariants };
|
||||
@@ -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": {
|
||||
|
||||
@@ -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::<models::App>(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<String> = 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<String> = 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::<models::Blog>(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<models::Blog> {
|
||||
use crate::db::schema::blog;
|
||||
|
||||
blog::table
|
||||
.load::<models::Blog>(conn)
|
||||
.expect("Error loading apps")
|
||||
}
|
||||
|
||||
pub fn get_apps_by_ids(conn: &mut SqliteConnection, ids: &[String]) -> Vec<models::App> {
|
||||
use crate::db::schema::app;
|
||||
|
||||
app::table
|
||||
.filter(app::id.eq_any(ids))
|
||||
.load::<models::App>(conn)
|
||||
.expect("Error loading apps by ids")
|
||||
}
|
||||
|
||||
pub fn get_blog_by_id(conn: &mut SqliteConnection, blog_id: &str) -> Option<models::Blog> {
|
||||
use crate::db::schema::blog;
|
||||
|
||||
blog::table
|
||||
.filter(blog::id.eq(blog_id))
|
||||
.first::<models::Blog>(conn)
|
||||
.optional()
|
||||
.expect("Error loading blog by id")
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ pub struct App {
|
||||
pub ssh_address: Option<String>, // 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<String>, // 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 {
|
||||
|
||||
+75
-7
@@ -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<App> {
|
||||
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<App> {
|
||||
|
||||
#[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<bool, String> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
struct TempBlog {
|
||||
id: String,
|
||||
title: String,
|
||||
content: String,
|
||||
linked_app_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_blogs() -> Vec<TempBlog> {
|
||||
// I'll need to decode the linked_app_ids from a json encoded string to a Vec<String>.
|
||||
let mut conn = establish_connection();
|
||||
let encoded_blogs = db::get_all_blogs(&mut conn);
|
||||
let mut decoded_blogs: Vec<TempBlog> = Vec::new();
|
||||
encoded_blogs.iter().for_each(|blog| {
|
||||
let linked_app_ids: Option<Vec<String>> = 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<Vec<String>>,
|
||||
) -> 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<String>) -> Vec<App> {
|
||||
let mut conn = establish_connection();
|
||||
db::get_apps_by_ids(&mut conn, &ids)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_blog(id: &str) -> Option<TempBlog> {
|
||||
let mut conn = establish_connection();
|
||||
let blog = db::get_blog_by_id(&mut conn, id);
|
||||
|
||||
blog.map(|b| {
|
||||
let linked_app_ids: Option<Vec<String>> = 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");
|
||||
|
||||
Vendored
+20
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user