A web app to help you design things, local, offline, on device. In your browser.
TypeScript
53.1%
JSON
45.6%
CSS
0.9%
Markdown
0.3%
JavaScript
0.1%
'use client';
import React, { useRef, useState } from 'react';
import { Upload, Image as ImageIcon, Link, Trash2 } from 'lucide-react';
import { useEditorStore } from '@/lib/store/editor-store';
import type { ImageLayerData, ScreenshotLayerData } from '@/types';
export function ImagesPanel() {
const fileInputRef = useRef<HTMLInputElement>(null);
const [uploadedImages, setUploadedImages] = useState<string[]>([]);
const [imageUrl, setImageUrl] = useState('');
const [dragActive, setDragActive] = useState(false);
const { addLayer, canvas, selectedLayerIds, layers, updateLayer } = useEditorStore();
const handleFileUpload = (files: FileList | null) => {
if (!files) return;
Array.from(files).forEach((file) => {
if (!file.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = (e) => {
const src = e.target?.result as string;
setUploadedImages((prev) => [...prev, src]);
// Auto-add to canvas if no device is selected
const selectedDevice = selectedLayerIds.find(
(id) => layers.find((l) => l.id === id)?.type === 'device'
);
if (!selectedDevice) {
addImageToCanvas(src);
} else {
// Add screenshot to device
addScreenshotToDevice(src, selectedDevice);
}
};
reader.readAsDataURL(file);
});
};
const addImageToCanvas = (src: string) => {
const img = new window.Image();
img.onload = () => {
// Calculate size to fit in canvas
const maxWidth = canvas.width * 0.6;
const maxHeight = canvas.height * 0.6;
const scale = Math.min(maxWidth / img.width, maxHeight / img.height);
const width = img.width * scale;
const height = img.height * scale;
const data: ImageLayerData = {
type: 'image',
src,
};
addLayer({
type: 'image',
name: 'Image',
visible: true,
locked: false,
opacity: 1,
position: {
x: (canvas.width - width) / 2,
y: (canvas.height - height) / 2,
},
size: { width, height },
rotation: 0,
data,
});
};
img.src = src;
};
const addScreenshotToDevice = (src: string, deviceLayerId: string) => {
const device = layers.find((l) => l.id === deviceLayerId);
if (!device || device.type !== 'device') return;
const data: ScreenshotLayerData = {
type: 'screenshot',
src,
fit: 'cover',
};
addLayer({
type: 'screenshot',
name: 'Screenshot',
visible: true,
locked: false,
opacity: 1,
position: device.position,
size: device.size,
rotation: device.rotation,
data,
});
};
const handleDrag = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.type === 'dragenter' || e.type === 'dragover') {
setDragActive(true);
} else if (e.type === 'dragleave') {
setDragActive(false);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
handleFileUpload(e.dataTransfer.files);
};
const handleUrlImport = () => {
if (!imageUrl) return;
setUploadedImages((prev) => [...prev, imageUrl]);
addImageToCanvas(imageUrl);
setImageUrl('');
};
const handleRemoveImage = (index: number) => {
setUploadedImages((prev) => prev.filter((_, i) => i !== index));
};
return (
<div className="p-4">
<h2 className="text-lg font-semibold text-white mb-4">Images & Screenshots</h2>
{/* Upload area */}
<div
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
className={`
relative p-6 rounded-xl border-2 border-dashed
transition-all cursor-pointer
${
dragActive
? 'border-indigo-500 bg-indigo-500/10'
: 'border-neutral-700 hover:border-neutral-600 bg-neutral-800/30'
}
`}
onClick={() => fileInputRef.current?.click()}
>
<input
ref={fileInputRef}
type="file"
accept="image/*"
multiple
className="hidden"
onChange={(e) => handleFileUpload(e.target.files)}
/>
<div className="flex flex-col items-center gap-2 text-center">
<div
className={`
w-12 h-12 rounded-xl flex items-center justify-center
${dragActive ? 'bg-indigo-500/20' : 'bg-neutral-700/50'}
`}
>
<Upload
className={`w-6 h-6 ${dragActive ? 'text-indigo-400' : 'text-neutral-400'}`}
/>
</div>
<div>
<p className="text-sm text-white">Drop images here</p>
<p className="text-xs text-neutral-500">or click to browse</p>
</div>
</div>
</div>
{/* URL import */}
<div className="mt-4">
<label className="text-sm text-neutral-400 block mb-2">Import from URL</label>
<div className="flex gap-2">
<input
type="url"
value={imageUrl}
onChange={(e) => setImageUrl(e.target.value)}
placeholder="https://..."
className="
flex-1 px-3 py-2 rounded-lg
bg-neutral-800 border border-neutral-700
text-white text-sm placeholder-neutral-500
focus:outline-none focus:border-indigo-500
"
/>
<button
onClick={handleUrlImport}
disabled={!imageUrl}
className="
px-3 py-2 rounded-lg
bg-indigo-500 hover:bg-indigo-600 disabled:bg-neutral-700
text-white disabled:text-neutral-500
transition-colors
"
>
<Link className="w-4 h-4" />
</button>
</div>
</div>
{/* Uploaded images gallery */}
{uploadedImages.length > 0 && (
<div className="mt-6">
<label className="text-sm text-neutral-400 block mb-2">Recent Uploads</label>
<div className="grid grid-cols-2 gap-2">
{uploadedImages.map((src, index) => (
<div
key={index}
className="relative group aspect-video rounded-lg overflow-hidden bg-neutral-800"
>
<img
src={src}
alt={`Upload ${index + 1}`}
className="w-full h-full object-cover"
/>
{/* Hover overlay */}
<div className="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-2">
<button
onClick={() => addImageToCanvas(src)}
className="p-2 rounded-lg bg-indigo-500 hover:bg-indigo-600 transition-colors"
>
<ImageIcon className="w-4 h-4 text-white" />
</button>
<button
onClick={() => handleRemoveImage(index)}
className="p-2 rounded-lg bg-red-500 hover:bg-red-600 transition-colors"
>
<Trash2 className="w-4 h-4 text-white" />
</button>
</div>
</div>
))}
</div>
</div>
)}
{/* Tips */}
<div className="mt-6 p-3 rounded-lg bg-neutral-800/50 border border-neutral-700/50">
<h3 className="text-sm font-medium text-white mb-2">Tips</h3>
<ul className="text-xs text-neutral-400 space-y-1">
<li>• Drop screenshots directly onto device mockups</li>
<li>• Use CMD/Ctrl + V to paste from clipboard</li>
<li>• Supports PNG, JPG, GIF, and WebP</li>
</ul>
</div>
</div>
);
}
About
A web app to help you design things, local, offline, on device. In your browser.
0 stars
0 forks