Files
lemonspace_app/components/canvas/nodes/image-node.tsx
Matthias ca40f5cb13 feat: enhance canvas and layout components with new features and improvements
- Added remote image patterns to the Next.js configuration for enhanced image handling.
- Updated TypeScript configuration to exclude the 'implement' directory.
- Refactored layout component to fetch initial authentication token and pass it to Providers.
- Replaced CanvasToolbar with CanvasSidebar for improved UI layout and functionality.
- Enhanced Canvas component with new drag-and-drop file upload capabilities and batch node movement.
- Updated various node components to support new status handling and improved user interactions.
- Added debounced saving for note and prompt nodes to optimize performance.
2026-03-25 17:58:58 +01:00

196 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import {
useState,
useCallback,
useRef,
type ChangeEvent,
type DragEvent,
} from "react";
import { Handle, Position, type NodeProps, type Node } from "@xyflow/react";
import { useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";
import type { Id } from "@/convex/_generated/dataModel";
import Image from "next/image";
import BaseNodeWrapper from "./base-node-wrapper";
type ImageNodeData = {
storageId?: string;
url?: string;
filename?: string;
mimeType?: string;
_status?: string;
_statusMessage?: string;
};
export type ImageNode = Node<ImageNodeData, "image">;
export default function ImageNode({ id, data, selected }: NodeProps<ImageNode>) {
const generateUploadUrl = useMutation(api.storage.generateUploadUrl);
const updateData = useMutation(api.nodes.updateData);
const fileInputRef = useRef<HTMLInputElement>(null);
const [isUploading, setIsUploading] = useState(false);
const [isDragOver, setIsDragOver] = useState(false);
const uploadFile = useCallback(
async (file: File) => {
if (!file.type.startsWith("image/")) return;
setIsUploading(true);
try {
const uploadUrl = await generateUploadUrl();
const result = await fetch(uploadUrl, {
method: "POST",
headers: { "Content-Type": file.type },
body: file,
});
if (!result.ok) {
throw new Error("Upload failed");
}
const { storageId } = (await result.json()) as { storageId: string };
await updateData({
nodeId: id as Id<"nodes">,
data: {
storageId,
filename: file.name,
mimeType: file.type,
},
});
} catch (err) {
console.error("Upload failed:", err);
} finally {
setIsUploading(false);
}
},
[id, generateUploadUrl, updateData]
);
const handleClick = useCallback(() => {
if (!data.url && !isUploading) {
fileInputRef.current?.click();
}
}, [data.url, isUploading]);
const handleFileChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) uploadFile(file);
},
[uploadFile]
);
const handleDragOver = useCallback((e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.dataTransfer.types.includes("Files")) {
setIsDragOver(true);
e.dataTransfer.dropEffect = "copy";
}
}, []);
const handleDragLeave = useCallback((e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
}, []);
const handleDrop = useCallback(
(e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
const file = e.dataTransfer.files?.[0];
if (file && file.type.startsWith("image/")) {
uploadFile(file);
}
},
[uploadFile]
);
const handleReplace = useCallback(() => {
fileInputRef.current?.click();
}, []);
return (
<BaseNodeWrapper selected={selected} status={data._status}>
<div className="p-2">
<div className="mb-1 flex items-center justify-between">
<div className="text-xs font-medium text-muted-foreground">🖼 Bild</div>
{data.url && (
<button
onClick={handleReplace}
className="nodrag text-xs text-muted-foreground transition-colors hover:text-foreground"
>
Ersetzen
</button>
)}
</div>
{isUploading ? (
<div className="flex h-36 w-56 items-center justify-center rounded-lg bg-muted">
<div className="flex flex-col items-center gap-2">
<div className="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent" />
<span className="text-xs text-muted-foreground">Wird hochgeladen...</span>
</div>
</div>
) : data.url ? (
<div className="relative h-36 w-56 overflow-hidden rounded-lg">
<Image
src={data.url}
alt={data.filename ?? "Bild"}
fill
className="object-cover"
sizes="224px"
draggable={false}
/>
</div>
) : (
<div
onClick={handleClick}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={`
nodrag flex h-36 w-56 cursor-pointer flex-col items-center justify-center
rounded-lg border-2 border-dashed text-sm transition-colors
${
isDragOver
? "border-primary bg-primary/5 text-primary"
: "text-muted-foreground hover:border-primary/50 hover:text-foreground"
}
`}
>
<span className="mb-1 text-lg">📁</span>
<span>Klicken oder hierhin ziehen</span>
<span className="mt-0.5 text-xs">PNG, JPG, WebP</span>
</div>
)}
{data.filename && data.url && (
<p className="mt-1 max-w-[260px] truncate text-xs text-muted-foreground">
{data.filename}
</p>
)}
</div>
<input
ref={fileInputRef}
type="file"
accept="image/png,image/jpeg,image/webp"
onChange={handleFileChange}
className="hidden"
/>
<Handle
type="source"
position={Position.Right}
className="h-3! w-3! bg-primary! border-2! border-background!"
/>
</BaseNodeWrapper>
);
}