Fraser's picture
upload folders?!
d081d7a
<script lang="ts">
interface Props {
onImageSelected: (image: File) => void;
onImagesSelected: (images: File[]) => void;
isProcessing?: boolean;
imageQueue?: File[];
currentImageIndex?: number;
}
let { onImageSelected, onImagesSelected, isProcessing = false, imageQueue = [], currentImageIndex = 0 }: Props = $props();
let fileInput: HTMLInputElement;
let dragActive = $state(false);
let preview: string | null = $state(null);
function handleFileSelect(e: Event) {
const target = e.target as HTMLInputElement;
if (target.files) {
if (target.files.length === 1) {
processFile(target.files[0]);
} else if (target.files.length > 1) {
processMultipleFiles(Array.from(target.files));
}
}
}
function handleDrop(e: DragEvent) {
e.preventDefault();
dragActive = false;
if (e.dataTransfer?.files) {
if (e.dataTransfer.files.length === 1) {
processFile(e.dataTransfer.files[0]);
} else if (e.dataTransfer.files.length > 1) {
processMultipleFiles(Array.from(e.dataTransfer.files));
}
}
}
function handleDragOver(e: DragEvent) {
e.preventDefault();
dragActive = true;
}
function handleDragLeave(e: DragEvent) {
e.preventDefault();
dragActive = false;
}
function processFile(file: File) {
if (!file.type.startsWith('image/')) {
alert('Please upload an image file');
return;
}
// Create preview
const reader = new FileReader();
reader.onload = (e) => {
preview = e.target?.result as string;
};
reader.readAsDataURL(file);
onImageSelected(file);
}
function processMultipleFiles(files: File[]) {
// Filter to only image files
const imageFiles = files.filter(file => file.type.startsWith('image/'));
if (imageFiles.length === 0) {
alert('Please upload at least one image file');
return;
}
if (imageFiles.length !== files.length) {
alert(`${files.length - imageFiles.length} non-image files were skipped. Processing ${imageFiles.length} images.`);
}
// Create preview from first image
const reader = new FileReader();
reader.onload = (e) => {
preview = e.target?.result as string;
};
reader.readAsDataURL(imageFiles[0]);
onImagesSelected(imageFiles);
}
function triggerFileSelect() {
fileInput.click();
}
</script>
<div class="upload-container">
<h3>Upload Your Photo{imageQueue.length > 1 ? 's' : ''}</h3>
<p class="subtitle">Upload {imageQueue.length > 1 ? 'photos' : 'a photo'} that will inspire your monster creation</p>
<!-- Image Queue Thumbnails -->
{#if imageQueue.length > 1}
<div class="queue-container">
<div class="queue-header">
<span class="queue-title">Processing {currentImageIndex + 1} of {imageQueue.length} images</span>
</div>
<div class="queue-thumbnails">
{#each imageQueue as image, index}
{@const isCurrentImage = index === currentImageIndex}
{@const isProcessed = index < currentImageIndex}
<div class="queue-thumbnail" class:current={isCurrentImage} class:processed={isProcessed}>
<img src={URL.createObjectURL(image)} alt="Queue thumbnail {index + 1}" />
<div class="thumbnail-overlay">
{#if isProcessed}
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor">
<path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/>
</svg>
{:else if isCurrentImage}
<div class="processing-indicator"></div>
{:else}
<span class="queue-number">{index + 1}</span>
{/if}
</div>
</div>
{/each}
</div>
</div>
{/if}
<div
class="upload-area"
class:drag-active={dragActive}
class:has-preview={preview}
ondrop={handleDrop}
ondragover={handleDragOver}
ondragleave={handleDragLeave}
onclick={triggerFileSelect}
onkeypress={(e) => e.key === 'Enter' && triggerFileSelect()}
role="button"
tabindex="0"
>
{#if preview}
<img src={preview} alt="Preview" class="preview-image" />
<div class="overlay">
<p>Click to change image</p>
</div>
{:else}
<svg class="upload-icon" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
<p class="upload-text">Drop an image here or click to upload</p>
<p class="upload-hint">Supports JPG, PNG, GIF</p>
{/if}
</div>
<input
type="file"
accept="image/*"
multiple
onchange={handleFileSelect}
bind:this={fileInput}
class="hidden-input"
disabled={isProcessing}
/>
</div>
<style>
.upload-container {
max-width: 600px;
margin: 0 auto;
text-align: center;
}
h3 {
margin-bottom: 0.5rem;
color: #333;
}
.subtitle {
color: #666;
margin-bottom: 2rem;
}
.upload-area {
border: 2px dashed #ccc;
border-radius: 12px;
padding: 3rem;
background: #fafafa;
transition: all 0.3s ease;
cursor: pointer;
position: relative;
overflow: hidden;
}
.upload-area:hover {
border-color: #007bff;
background: #f0f7ff;
}
.upload-area.drag-active {
border-color: #007bff;
background: #e3f2ff;
transform: scale(1.02);
}
.upload-area.has-preview {
padding: 0;
background: #fff;
}
.upload-icon {
color: #007bff;
margin-bottom: 1rem;
}
.upload-text {
font-size: 1.1rem;
color: #333;
margin-bottom: 0.5rem;
}
.upload-hint {
font-size: 0.9rem;
color: #666;
}
.hidden-input {
display: none;
}
.preview-image {
width: 100%;
height: 400px;
object-fit: contain;
display: block;
}
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.upload-area:hover .overlay {
opacity: 1;
}
.overlay p {
color: white;
font-size: 1.1rem;
}
/* Queue Thumbnails */
.queue-container {
margin-bottom: 2rem;
padding: 1rem;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.queue-header {
margin-bottom: 1rem;
text-align: center;
}
.queue-title {
font-weight: 600;
color: #495057;
font-size: 0.9rem;
}
.queue-thumbnails {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: center;
}
.queue-thumbnail {
position: relative;
width: 80px;
height: 80px;
border-radius: 8px;
overflow: hidden;
border: 2px solid #dee2e6;
transition: all 0.3s ease;
}
.queue-thumbnail.current {
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.2);
}
.queue-thumbnail.processed {
border-color: #28a745;
opacity: 0.8;
}
.queue-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
.thumbnail-overlay {
position: absolute;
top: 0;
right: 0;
width: 24px;
height: 24px;
background: rgba(0, 0, 0, 0.7);
border-radius: 0 6px 0 6px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.75rem;
font-weight: 600;
}
.queue-thumbnail.processed .thumbnail-overlay {
background: #28a745;
}
.queue-thumbnail.current .thumbnail-overlay {
background: #007bff;
}
.processing-indicator {
width: 12px;
height: 12px;
border: 2px solid #ffffff;
border-top: 2px solid transparent;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.queue-number {
font-size: 0.7rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>