carbon-tokenization / frontend /src /editor /ImageUploadView.tsx
tfrere's picture
tfrere HF Staff
chore: initial commit
561e6f0
import { NodeViewWrapper } from "@tiptap/react";
import { useCallback, useRef, useState } from "react";
import type { NodeViewProps } from "@tiptap/react";
import { uploadImage } from "./upload";
export function ImageUploadView({ editor, getPos }: NodeViewProps) {
const [dragging, setDragging] = useState(false);
const [uploading, setUploading] = useState(false);
const [urlMode, setUrlMode] = useState(false);
const [urlValue, setUrlValue] = useState("");
const fileRef = useRef<HTMLInputElement>(null);
const replaceWithImage = useCallback(
(src: string) => {
const pos = getPos();
if (pos === undefined) return;
editor
.chain()
.focus()
.deleteRange({ from: pos, to: pos + 1 })
.insertContentAt(pos, {
type: "image",
attrs: { src },
})
.run();
},
[editor, getPos],
);
const handleFiles = useCallback(
(files: FileList | File[]) => {
const file = Array.from(files).find((f) => f.type.startsWith("image/"));
if (!file) return;
setUploading(true);
uploadImage(file)
.then((url) => replaceWithImage(url))
.catch((err) => {
console.error("[image-upload]", err);
setUploading(false);
});
},
[replaceWithImage],
);
const onDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
setDragging(false);
if (e.dataTransfer.files.length) handleFiles(e.dataTransfer.files);
},
[handleFiles],
);
const onDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
setDragging(true);
}, []);
const onDragLeave = useCallback(() => setDragging(false), []);
const submitUrl = useCallback(() => {
const src = urlValue.trim();
if (src) replaceWithImage(src);
}, [urlValue, replaceWithImage]);
return (
<NodeViewWrapper>
<div
className={`image-upload-card${dragging ? " dragging" : ""}`}
onDrop={onDrop}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
contentEditable={false}
>
<input
ref={fileRef}
type="file"
accept="image/*"
style={{ display: "none" }}
onChange={(e) => {
if (e.target.files?.length) handleFiles(e.target.files);
}}
/>
{uploading ? (
<>
<div className="image-upload-card-icon" style={{ opacity: 0.5 }}>
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<circle cx="8.5" cy="8.5" r="1.5" />
<polyline points="21 15 16 10 5 21" />
</svg>
</div>
<p className="image-upload-card-hint">Uploading...</p>
</>
) : !urlMode ? (
<>
<div className="image-upload-card-icon">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<circle cx="8.5" cy="8.5" r="1.5" />
<polyline points="21 15 16 10 5 21" />
</svg>
</div>
<div className="image-upload-card-actions">
<button
className="image-upload-btn primary"
onClick={() => fileRef.current?.click()}
>
Upload image
</button>
<button
className="image-upload-btn secondary"
onClick={() => setUrlMode(true)}
>
Embed link
</button>
</div>
<p className="image-upload-card-hint">
Drag & drop an image, or click to upload
</p>
</>
) : (
<div className="image-upload-card-url">
<input
type="text"
className="image-upload-url-input"
placeholder="Paste image URL..."
value={urlValue}
onChange={(e) => setUrlValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") submitUrl();
if (e.key === "Escape") setUrlMode(false);
}}
autoFocus
/>
<div className="image-upload-card-actions">
<button className="image-upload-btn primary" onClick={submitUrl}>
Embed
</button>
<button
className="image-upload-btn secondary"
onClick={() => setUrlMode(false)}
>
Cancel
</button>
</div>
</div>
)}
</div>
</NodeViewWrapper>
);
}