WIP: blogging. you have a markdown editor with linking capabilities, along with the ability to view, edit, and create other blogs. #1

Draft
pure_sagacity wants to merge 5 commits from blogging into main
5 changed files with 86 additions and 52 deletions
Showing only changes of commit 8134ec117f - Show all commits
+26 -10
View File
@@ -1,12 +1,12 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState, Suspense } from "react";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import TextEditor from "@/components/text-editor"; import TextEditor from "@/components/text-editor";
import { HomelabApp } from "@/types"; // Adjust path if needed import { HomelabApp } from "@/types";
export default function WritePage() { function WritePageContent() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const appQuery = searchParams.get("app"); const appQuery = searchParams.get("app");
@@ -21,12 +21,13 @@ export default function WritePage() {
} }
try { try {
// Fetch apps to find the one matching the query parameter string
const apps = (await invoke("get_app")) as HomelabApp[]; const apps = (await invoke("get_app")) as HomelabApp[];
// Match checking both UUID structure and sanitized string names
const matchedApp = apps.find( const matchedApp = apps.find(
(app) => (app) =>
app.id === appQuery || app.id === appQuery ||
app.name.toLowerCase() === appQuery.toLowerCase(), app.name.toLowerCase().trim() === appQuery.toLowerCase().trim(),
); );
if (matchedApp) { if (matchedApp) {
@@ -56,13 +57,28 @@ export default function WritePage() {
); );
} }
return (
<TextEditor
content=""
onSave={onSave}
initialLinkedAppIds={initialAppIds}
/>
);
}
// 2. Main Page export wrapped in a Suspense boundary
export default function WritePage() {
return ( return (
<div className="flex flex-col p-2"> <div className="flex flex-col p-2">
<TextEditor <Suspense
content="" fallback={
onSave={onSave} <div className="p-4 text-sm text-zinc-400">
initialLinkedAppIds={initialAppIds} Loading router state...
/> </div>
}
>
<WritePageContent />
</Suspense>
</div> </div>
); );
} }
+6 -1
View File
@@ -11,6 +11,7 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import DeleteDialog from "./delete-dialog"; import DeleteDialog from "./delete-dialog";
import { useState } from "react"; import { useState } from "react";
import { useRouter } from "next/navigation";
type Props = { type Props = {
id: string; id: string;
@@ -22,6 +23,8 @@ export default function ActionMenu({ id }: Props) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const router = useRouter();
return ( return (
<> <>
<TooltipProvider delayDuration={150}> <TooltipProvider delayDuration={150}>
@@ -69,7 +72,9 @@ export default function ActionMenu({ id }: Props) {
<button <button
className={buttonClass} className={buttonClass}
onClick={() => { onClick={() => {
// Blog let link = `/write/new?app=${id}`;
console.log(link);
router.push(link);
}} }}
> >
<Pencil className="h-5 w-5" /> <Pencil className="h-5 w-5" />
+1 -5
View File
@@ -90,11 +90,7 @@ export default function AppCard({ app }: Props) {
</div> </div>
<h3 className="text-2xl font-bold">{app.name}</h3> <h3 className="text-2xl font-bold">{app.name}</h3>
<a <a target="_blank" href={`http://${app.address}`}>
className={`${hovered && "text-blue-600"} transition-colors duration-200`}
target="_blank"
href={`http://${app.address}`}
>
{app.address} {app.address}
</a> </a>
+16 -3
View File
@@ -20,10 +20,21 @@ const getApps = async (): Promise<HomelabApp[]> => {
return (await invoke("get_app")) as HomelabApp[]; return (await invoke("get_app")) as HomelabApp[];
}; };
export default function TextEditor({ content, onSave }: TextEditorProps) { export default function TextEditor({
content,
onSave,
initialLinkedAppIds = [],
}: TextEditorProps) {
const [availableApps, setAvailableApps] = useState<HomelabApp[]>([]); const [availableApps, setAvailableApps] = useState<HomelabApp[]>([]);
// Track document-wide linked apps here const [linkedAppIds, setLinkedAppIds] =
const [linkedAppIds, setLinkedAppIds] = useState<string[]>([]); useState<string[]>(initialLinkedAppIds);
// 🔄 FIX: Watch for updates to initialLinkedAppIds once fetched from the URL
useEffect(() => {
if (initialLinkedAppIds.length > 0) {
setLinkedAppIds(initialLinkedAppIds);
}
}, [initialLinkedAppIds]);
useEffect(() => { useEffect(() => {
const fetchApps = async () => { const fetchApps = async () => {
@@ -33,6 +44,8 @@ export default function TextEditor({ content, onSave }: TextEditorProps) {
fetchApps(); fetchApps();
}, []); }, []);
// ... rest of your TextEditor component stays the same
const editor = useEditor({ const editor = useEditor({
extensions: [ extensions: [
StarterKit.configure({ StarterKit.configure({
+37 -33
View File
@@ -14,80 +14,84 @@ import {
} from "lucide-react"; } from "lucide-react";
import { Toggle } from "../ui/toggle"; import { Toggle } from "../ui/toggle";
export default function Menubar({ editor }: { editor: Editor | null }) { 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) { if (!editor) {
return null; 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({ const editorState = useEditorState({
editor, editor, // Now safely guaranteed never to be null or undefined
selector: (ctx) => { selector: (ctx) => {
return { return {
// Text formatting isBold: ctx.editor.isActive("bold"),
isBold: ctx.editor.isActive("bold") ?? false, isItalic: ctx.editor.isActive("italic"),
isItalic: ctx.editor.isActive("italic") ?? false, isStrike: ctx.editor.isActive("strike"),
isStrike: ctx.editor.isActive("strike") ?? false, isHighlight: ctx.editor.isActive("highlight"),
isHighlight: ctx.editor.isActive("highlight") ?? false,
// Text alignment isAlignLeft: ctx.editor.isActive({ textAlign: "left" }),
isAlignLeft: ctx.editor.isActive({ textAlign: "left" }) ?? false, isAlignCenter: ctx.editor.isActive({ textAlign: "center" }),
isAlignCenter: ctx.editor.isActive({ textAlign: "center" }) ?? false, isAlignRight: ctx.editor.isActive({ textAlign: "right" }),
isAlignRight: ctx.editor.isActive({ textAlign: "right" }) ?? false, isAlignJustify: ctx.editor.isActive({ textAlign: "justify" }),
isAlignJustify: ctx.editor.isActive({ textAlign: "justify" }) ?? false,
// Block types isParagraph: ctx.editor.isActive("paragraph"),
isParagraph: ctx.editor.isActive("paragraph") ?? false, isHeading1: ctx.editor.isActive("heading", { level: 1 }),
isHeading1: ctx.editor.isActive("heading", { level: 1 }) ?? false, isHeading2: ctx.editor.isActive("heading", { level: 2 }),
isHeading2: ctx.editor.isActive("heading", { level: 2 }) ?? false, isHeading3: ctx.editor.isActive("heading", { level: 3 }),
isHeading3: ctx.editor.isActive("heading", { level: 3 }) ?? false,
}; };
}, },
}); });
const options: { const options = [
icon: React.ReactNode;
onClick: () => void;
pressed: boolean;
}[] = [
{ {
icon: <Heading1 className="size-4" />, icon: <Heading1 className="size-4" />,
onClick: () => editor.chain().focus().toggleHeading({ level: 1 }).run(), onClick: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
pressed: editor.isActive("heading", { level: 1 }), pressed: editorState.isHeading1,
}, },
{ {
icon: <Heading2 className="size-4" />, icon: <Heading2 className="size-4" />,
onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run(), onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
pressed: editor.isActive("heading", { level: 2 }), pressed: editorState.isHeading2,
}, },
{ {
icon: <Heading3 className="size-4" />, icon: <Heading3 className="size-4" />,
onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(), onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
pressed: editor.isActive("heading", { level: 3 }), pressed: editorState.isHeading3,
}, },
{ {
icon: <Bold className="size-4" />, icon: <Bold className="size-4" />,
onClick: () => editor.chain().focus().toggleBold().run(), onClick: () => editor.chain().focus().toggleBold().run(),
pressed: editor.isActive("bold"), pressed: editorState.isBold,
}, },
{ {
icon: <Italic className="size-4" />, icon: <Italic className="size-4" />,
onClick: () => editor.chain().focus().toggleItalic().run(), onClick: () => editor.chain().focus().toggleItalic().run(),
pressed: editor.isActive("italic"), pressed: editorState.isItalic,
}, },
{ {
icon: <AlignLeft className="size-4" />, icon: <AlignLeft className="size-4" />,
onClick: () => editor.chain().focus().toggleTextAlign("left").run(), onClick: () => editor.chain().focus().toggleTextAlign("left").run(),
pressed: editor.isActive({ textAlign: "left" }), pressed: editorState.isAlignLeft,
}, },
{ {
icon: <AlignCenter className="size-4" />, icon: <AlignCenter className="size-4" />,
onClick: () => editor.chain().focus().toggleTextAlign("center").run(), onClick: () => editor.chain().focus().toggleTextAlign("center").run(),
pressed: editor.isActive({ textAlign: "center" }), pressed: editorState.isAlignCenter,
}, },
{ {
icon: <AlignRight className="size-4" />, icon: <AlignRight className="size-4" />,
onClick: () => editor.chain().focus().toggleTextAlign("right").run(), onClick: () => editor.chain().focus().toggleTextAlign("right").run(),
pressed: editor.isActive({ textAlign: "right" }), pressed: editorState.isAlignRight,
}, },
{ {
icon: <List className="size-4" />, icon: <List className="size-4" />,
@@ -97,17 +101,17 @@ export default function Menubar({ editor }: { editor: Editor | null }) {
{ {
icon: <ListOrdered className="size-4" />, icon: <ListOrdered className="size-4" />,
onClick: () => editor.chain().focus().toggleOrderedList().run(), onClick: () => editor.chain().focus().toggleOrderedList().run(),
pressed: editor.isActive("orderList"), pressed: editor.isActive("orderedList"),
}, },
{ {
icon: <Highlighter className="size-4" />, icon: <Highlighter className="size-4" />,
onClick: () => editor.chain().focus().toggleHighlight().run(), onClick: () => editor.chain().focus().toggleHighlight().run(),
pressed: editor.isActive("highlight"), pressed: editorState.isHighlight,
}, },
]; ];
return ( return (
<div className="border rounded-md p-1 mb-1 bg-slate-50 space-x-2 z-50"> <div className="border rounded-md p-1 mb-1 bg-slate-50 space-x-2 z-50 flex items-center">
{options.map((option, index) => ( {options.map((option, index) => (
<Toggle key={index} pressed={option.pressed} onClick={option.onClick}> <Toggle key={index} pressed={option.pressed} onClick={option.onClick}>
{option.icon} {option.icon}