diff --git a/app/javascript/controllers/file_drop_controller.js b/app/javascript/controllers/file_drop_controller.js new file mode 100644 index 0000000..a9132c8 --- /dev/null +++ b/app/javascript/controllers/file_drop_controller.js @@ -0,0 +1,96 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["input", "dropzone", "preview", "previewImage", "filename", "filesize"] + + connect() { + // Prevent default drag behaviors on the whole document + ["dragenter", "dragover", "dragleave", "drop"].forEach(eventName => { + document.body.addEventListener(eventName, this.preventDefaults, false) + }) + } + + disconnect() { + ["dragenter", "dragover", "dragleave", "drop"].forEach(eventName => { + document.body.removeEventListener(eventName, this.preventDefaults, false) + }) + } + + preventDefaults(e) { + e.preventDefault() + e.stopPropagation() + } + + dragover(e) { + e.preventDefault() + e.stopPropagation() + this.dropzoneTarget.classList.add("border-blue-500", "bg-blue-50") + } + + dragleave(e) { + e.preventDefault() + e.stopPropagation() + this.dropzoneTarget.classList.remove("border-blue-500", "bg-blue-50") + } + + drop(e) { + e.preventDefault() + e.stopPropagation() + this.dropzoneTarget.classList.remove("border-blue-500", "bg-blue-50") + + const files = e.dataTransfer.files + if (files.length > 0) { + // Set the file to the input element + this.inputTarget.files = files + this.handleFiles() + } + } + + handleFiles() { + const file = this.inputTarget.files[0] + if (!file) return + + // Validate file type + const validTypes = ["image/png", "image/jpg", "image/jpeg", "image/gif", "image/svg+xml"] + if (!validTypes.includes(file.type)) { + alert("Please upload a PNG, JPG, GIF, or SVG image") + this.clear() + return + } + + // Validate file size (2MB) + if (file.size > 2 * 1024 * 1024) { + alert("File size must be less than 2MB") + this.clear() + return + } + + // Show preview + this.filenameTarget.textContent = file.name + this.filesizeTarget.textContent = this.formatFileSize(file.size) + + // Create preview image + const reader = new FileReader() + reader.onload = (e) => { + this.previewImageTarget.src = e.target.result + this.previewTarget.classList.remove("hidden") + } + reader.readAsDataURL(file) + } + + clear(e) { + if (e) { + e.preventDefault() + } + this.inputTarget.value = "" + this.previewTarget.classList.add("hidden") + } + + formatFileSize(bytes) { + if (bytes === 0) return "0 Bytes" + const k = 1024 + const sizes = ["Bytes", "KB", "MB"] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return Math.round(bytes / Math.pow(k, i) * 100) / 100 + " " + sizes[i] + } +} diff --git a/config/initializers/active_storage.rb b/config/initializers/active_storage.rb new file mode 100644 index 0000000..10a440d --- /dev/null +++ b/config/initializers/active_storage.rb @@ -0,0 +1,14 @@ +# Configure ActiveStorage content type resolution +Rails.application.config.after_initialize do + # Ensure SVG files are served with the correct content type + ActiveStorage::Blob.class_eval do + def content_type_for_serving + # Override content type for SVG files + if filename.extension == "svg" && content_type == "application/octet-stream" + "image/svg+xml" + else + content_type + end + end + end +end diff --git a/db/migrate/20251125063248_create_active_storage_tables.active_storage.rb b/db/migrate/20251125063248_create_active_storage_tables.active_storage.rb new file mode 100644 index 0000000..6bd8bd0 --- /dev/null +++ b/db/migrate/20251125063248_create_active_storage_tables.active_storage.rb @@ -0,0 +1,57 @@ +# This migration comes from active_storage (originally 20170806125915) +class CreateActiveStorageTables < ActiveRecord::Migration[7.0] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + create_table :active_storage_blobs, id: primary_key_type do |t| + t.string :key, null: false + t.string :filename, null: false + t.string :content_type + t.text :metadata + t.string :service_name, null: false + t.bigint :byte_size, null: false + t.string :checksum + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + t.index [ :key ], unique: true + end + + create_table :active_storage_attachments, id: primary_key_type do |t| + t.string :name, null: false + t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type + t.references :blob, null: false, type: foreign_key_type + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + + create_table :active_storage_variant_records, id: primary_key_type do |t| + t.belongs_to :blob, null: false, index: false, type: foreign_key_type + t.string :variation_digest, null: false + + t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + end + + private + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [ primary_key_type, foreign_key_type ] + end +end