deckhearth/components/UploadImageModal.js

199 lines
7.3 KiB
JavaScript
Raw Normal View History

/* eslint-disable @next/next/no-img-element -- Local preview data URLs; next/image migration is out of scope. */
🚀 Implemented Complete Collection Functionality Built out all requested features from top to bottom: ✅ Upload Modal for Hero Images: - Created UploadImageModal component with drag-and-drop - Support for both URL input and file upload - Live preview and validation - Integrated into collection detail page ✅ Smart TCG Tags: - Dynamic tags showing only games with cards - Properly positioned under description - Clean blue rounded styling ✅ Combined Share & Invite Modal: - Unified ShareModal replacing separate buttons - Public access toggle with community visibility - Email/member search functionality - Default viewer role for invitations - Social sharing (Twitter, Facebook, Reddit, Discord) - User search API endpoint (/api/users/search) ✅ Comprehensive Favorites System: - Database schema for cards, collections, and decks - API endpoint (/api/favorites) for CRUD operations - Real-time favorite status checking - Working toggle functionality in UI - Migration script for database setup ✅ CSV Download Functionality: - Complete card metadata export - Proper CSV formatting with escaping - All card fields included (name, set, rarity, etc.) - Automatic filename generation - Client-side download implementation 🎯 UI/UX Improvements: - Removed duplicate buttons and switches - Clean action bar with proper hierarchy - Working modals with proper state management - Error handling and loading states 🛠️ Technical Features: - JWT authentication for all endpoints - Proper database relationships and indexes - CORS headers and error handling - Optimized queries and performance All todos completed! Ready for full collection management! 🎮✨
2025-07-25 23:28:52 -04:00
import { useState } from 'react';
export default function UploadImageModal({ isOpen, onClose, onUpload, currentImage }) {
const [uploadMethod, setUploadMethod] = useState('url'); // 'url' or 'file'
const [imageUrl, setImageUrl] = useState(currentImage || '');
const [dragActive, setDragActive] = useState(false);
const [uploading, setUploading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
if (!imageUrl.trim()) return;
setUploading(true);
try {
await onUpload(imageUrl);
onClose();
} catch (error) {
console.error('Upload failed:', error);
} finally {
setUploading(false);
}
};
const handleDrag = (e) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive(true);
} else if (e.type === "dragleave") {
setDragActive(false);
}
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
handleFileUpload(e.dataTransfer.files[0]);
}
};
const handleFileUpload = (file) => {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
setImageUrl(e.target.result);
setUploadMethod('file');
};
reader.readAsDataURL(file);
}
};
const handleFileInput = (e) => {
if (e.target.files && e.target.files[0]) {
handleFileUpload(e.target.files[0]);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-bold text-gray-900">Upload Hero Image</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form onSubmit={handleSubmit}>
{/* Upload Method Tabs */}
<div className="flex space-x-1 mb-6 bg-gray-100 rounded-lg p-1">
<button
type="button"
onClick={() => setUploadMethod('url')}
className={`flex-1 py-2 px-3 rounded-md text-sm font-medium transition-colors ${
uploadMethod === 'url'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
Image URL
</button>
<button
type="button"
onClick={() => setUploadMethod('file')}
className={`flex-1 py-2 px-3 rounded-md text-sm font-medium transition-colors ${
uploadMethod === 'file'
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
Upload File
</button>
</div>
{uploadMethod === 'url' ? (
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
Image URL
</label>
<input
type="url"
value={imageUrl}
onChange={(e) => setImageUrl(e.target.value)}
placeholder="https://example.com/image.jpg"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
) : (
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
Upload Image
</label>
<div
className={`border-2 border-dashed rounded-lg p-6 text-center transition-colors ${
dragActive
? 'border-purple-500 bg-purple-50'
: 'border-gray-300 hover:border-gray-400'
}`}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
>
<svg className="w-12 h-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<p className="text-gray-600 mb-2">Drag and drop an image here, or</p>
<label className="inline-block px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 cursor-pointer transition-colors">
Choose File
<input
type="file"
accept="image/*"
onChange={handleFileInput}
className="hidden"
/>
</label>
<p className="text-xs text-gray-500 mt-2">PNG, JPG, GIF up to 10MB</p>
</div>
</div>
)}
{/* Preview */}
{imageUrl && (
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
Preview
</label>
<div className="relative rounded-lg overflow-hidden bg-gray-100">
<img
src={imageUrl}
alt="Preview"
className="w-full h-32 object-cover"
onError={(e) => {
e.target.src = 'https://via.placeholder.com/400x128/f3f4f6/6b7280?text=Invalid+Image';
}}
/>
</div>
</div>
)}
{/* Actions */}
<div className="flex space-x-3">
<button
type="button"
onClick={onClose}
className="flex-1 py-2 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={!imageUrl.trim() || uploading}
className={`flex-1 py-2 px-4 rounded-lg font-medium transition-colors ${
!imageUrl.trim() || uploading
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
: 'bg-purple-600 text-white hover:bg-purple-700'
}`}
>
{uploading ? 'Uploading...' : 'Upload Image'}
</button>
</div>
</form>
</div>
</div>
</div>
);
}