Files
ProjectE/apps/web/components/reports/report-editor.tsx
T
Hermes 7297b7075e fix: reports API - add DB table, rewrite routes with Drizzle, fix TipTap type errors
Added reports table to PostgreSQL and Drizzle schema. Rewrote reports API routes to use Drizzle ORM instead of PocketBase stub. Fixed shared schema to match frontend field names. Fixed TipTap v3 type errors in note-editor.tsx and report-editor.tsx (StarterKit cast+chain as any).
2026-08-02 02:22:31 +00:00

182 lines
4.9 KiB
TypeScript

'use client';
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKitRaw from '@tiptap/starter-kit';
const StarterKit = StarterKitRaw as any;
import Link from '@tiptap/extension-link';
import Placeholder from '@tiptap/extension-placeholder';
import { useEffect } from 'react';
import { Button } from '@/components/ui/button';
import {
Bold,
Italic,
Strikethrough,
Code,
List,
ListOrdered,
Quote,
Undo,
Redo,
Heading1,
Heading2,
} from 'lucide-react';
import { cn } from '@/lib/utils';
interface ReportEditorProps {
content: string;
onChange: (content: string) => void;
onBlur?: () => void;
}
function ToolbarButton({
onClick,
active,
disabled,
label,
children,
}: {
onClick: () => void;
active?: boolean;
disabled?: boolean;
label: string;
children: React.ReactNode;
}) {
return (
<Button
variant="ghost"
size="icon"
className={cn('h-8 w-8', active && 'bg-accent text-accent-foreground')}
onClick={onClick}
disabled={disabled}
aria-label={label}
aria-pressed={active}
>
{children}
</Button>
);
}
export function ReportEditor({ content, onChange, onBlur }: ReportEditorProps) {
const editor = useEditor({
extensions: [
StarterKit,
Link.configure({
openOnClick: false,
}),
Placeholder.configure({
placeholder: 'Start writing your report...',
}),
],
content,
onUpdate: ({ editor: e }) => {
onChange(e.getHTML());
},
onBlur: () => {
onBlur?.();
},
});
useEffect(() => {
if (editor && content !== editor.getHTML()) {
editor.commands.setContent(content);
}
}, [content, editor]);
if (!editor) {
return null;
}
return (
<div className="flex h-full flex-col">
{/* Toolbar */}
<div className="mb-4 flex flex-wrap gap-1 border-b pb-2">
<ToolbarButton
onClick={() => (editor.chain().focus() as any).toggleHeading({ level: 1 }).run()}
active={editor.isActive('heading', { level: 1 })}
label="Heading 1"
>
<Heading1 className="h-4 w-4" />
</ToolbarButton>
<ToolbarButton
onClick={() => (editor.chain().focus() as any).toggleHeading({ level: 2 }).run()}
active={editor.isActive('heading', { level: 2 })}
label="Heading 2"
>
<Heading2 className="h-4 w-4" />
</ToolbarButton>
<div className="mx-1 w-px bg-border" />
<ToolbarButton
onClick={() => (editor.chain().focus() as any).toggleBold().run()}
active={editor.isActive('bold')}
label="Bold"
>
<Bold className="h-4 w-4" />
</ToolbarButton>
<ToolbarButton
onClick={() => (editor.chain().focus() as any).toggleItalic().run()}
active={editor.isActive('italic')}
label="Italic"
>
<Italic className="h-4 w-4" />
</ToolbarButton>
<ToolbarButton
onClick={() => (editor.chain().focus() as any).toggleStrike().run()}
active={editor.isActive('strike')}
label="Strikethrough"
>
<Strikethrough className="h-4 w-4" />
</ToolbarButton>
<ToolbarButton
onClick={() => (editor.chain().focus() as any).toggleCode().run()}
active={editor.isActive('code')}
label="Code"
>
<Code className="h-4 w-4" />
</ToolbarButton>
<div className="mx-1 w-px bg-border" />
<ToolbarButton
onClick={() => (editor.chain().focus() as any).toggleBulletList().run()}
active={editor.isActive('bulletList')}
label="Bullet list"
>
<List className="h-4 w-4" />
</ToolbarButton>
<ToolbarButton
onClick={() => (editor.chain().focus() as any).toggleOrderedList().run()}
active={editor.isActive('orderedList')}
label="Ordered list"
>
<ListOrdered className="h-4 w-4" />
</ToolbarButton>
<ToolbarButton
onClick={() => (editor.chain().focus() as any).toggleBlockquote().run()}
active={editor.isActive('blockquote')}
label="Blockquote"
>
<Quote className="h-4 w-4" />
</ToolbarButton>
<div className="mx-1 w-px bg-border" />
<ToolbarButton
onClick={() => (editor.chain().focus() as any).undo().run()}
disabled={!editor.can().undo()}
label="Undo"
>
<Undo className="h-4 w-4" />
</ToolbarButton>
<ToolbarButton
onClick={() => (editor.chain().focus() as any).redo().run()}
disabled={!editor.can().redo()}
label="Redo"
>
<Redo className="h-4 w-4" />
</ToolbarButton>
</div>
{/* Editor content */}
<div className="flex-1 overflow-auto">
<EditorContent editor={editor} className="tiptap-editor min-h-[400px]" />
</div>
</div>
);
}