Files
hudozka/apps/hudozka-writer/src/nodes/ImageBlock.tsx
T
tmshv 467a218d00 move to biome (#297)
* switch to biome

* writer format

* migrate format

* packages format

* hudozka format

* writer fix

* biome config updated

* migrate fix

* packages fix

* hudozka fix

* claude updated
2026-04-25 03:25:15 +03:00

114 lines
4.2 KiB
TypeScript

import { mergeAttributes, Node } from "@tiptap/core"
import type { NodeViewProps } from "@tiptap/react"
import { NodeViewWrapper, ReactNodeViewRenderer } from "@tiptap/react"
import { useEffect, useState } from "react"
import { BlockActions } from "../components/BlockActions"
import { BlockInsert } from "../components/BlockInsert"
import { ImagePicker } from "../components/ImagePicker"
import { pb } from "../pb"
import type { PbImage } from "../types"
function ImageBlockView({ node, updateAttributes, editor, getPos }: NodeViewProps) {
const { imageId, wide, caption } = node.attrs
const [thumbnail, setThumbnail] = useState<string | null>(null)
const [showPicker, setShowPicker] = useState(false)
useEffect(() => {
if (!imageId) return
pb.collection("images")
.getOne<PbImage>(imageId)
.then(img => {
const url = pb.files.getURL(img, img.file, { thumb: "300x200" })
setThumbnail(url)
})
.catch(() => {
setThumbnail(null)
})
}, [imageId])
function handleSelect(newImageId: string) {
updateAttributes({ imageId: newImageId })
setShowPicker(false)
}
return (
<>
<NodeViewWrapper className="node-view-block node-image-block">
<BlockActions editor={editor} getPos={getPos} />
<div className="node-view-content">
<div className="node-block-label">Image</div>
{thumbnail ? (
<button
type="button"
className="node-image-thumb-button"
onClick={() => setShowPicker(true)}
style={{ cursor: "pointer", padding: 0, border: 0, background: "none" }}
>
<img src={thumbnail} alt={caption || "image"} className="node-image-thumb" />
</button>
) : (
<button
type="button"
className="node-image-placeholder"
onClick={() => setShowPicker(true)}
style={{ cursor: "pointer" }}
>
{imageId ? "Loading..." : "Click to select image"}
</button>
)}
<div className="node-image-controls">
<label>
<input
type="checkbox"
checked={wide}
onChange={e => updateAttributes({ wide: e.target.checked })}
/>
Wide
</label>
<input
type="text"
placeholder="Caption"
value={caption || ""}
onChange={e => updateAttributes({ caption: e.target.value })}
className="node-image-caption"
/>
<button type="button" className="node-image-pick-btn" onClick={() => setShowPicker(true)}>
Pick
</button>
</div>
{showPicker && <ImagePicker onSelect={handleSelect} onClose={() => setShowPicker(false)} />}
</div>
</NodeViewWrapper>
<BlockInsert editor={editor} getPos={getPos} />
</>
)
}
export const ImageBlock = Node.create({
name: "imageBlock",
group: "block",
atom: true,
draggable: true,
addAttributes() {
return {
id: { default: null },
imageId: { default: "" },
wide: { default: false },
caption: { default: "" },
}
},
parseHTML() {
return [{ tag: "div[data-image-block]" }]
},
renderHTML({ HTMLAttributes }) {
return ["div", mergeAttributes(HTMLAttributes, { "data-image-block": "" })]
},
addNodeView() {
return ReactNodeViewRenderer(ImageBlockView)
},
})