Explorar el Código

Add image preview in gallery, cart/order thumbnails, block add-to-cart without photo — v1.0.4 -> v1.0.6

- Replace product gallery image with uploaded photo preview on upload,
  restore original on remove
- Replace cart item thumbnail with uploaded photo (woocommerce_cart_item_thumbnail)
- Replace order item thumbnail in admin (woocommerce_admin_order_item_thumbnail)
- Upload response now includes preview_url (woocommerce_single size)
- JS intercepts all add-to-cart buttons/links/forms and blocks
  if no photo uploaded, with scroll to upload area
- Fix upload freeze: wrap wp_generate_attachment_metadata safely
- Update translations (EN/CZ), documentation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dalibor Votruba hace 3 meses
padre
commit
afe1f4f218

+ 9 - 6
studiou-wc-free-photo-product/CLAUDE.md

@@ -2,7 +2,7 @@
 
 ## Project Overview
 
-WordPress/WooCommerce plugin called **studiou-wc-free-photo-product** (QDR - Studiou WC Free Photo Product) v1.0.3.
+WordPress/WooCommerce plugin called **studiou-wc-free-photo-product** (QDR - Studiou WC Free Photo Product) v1.0.6.
 Allows publishing special variable products where customers upload custom raw images to be printed for a price.
 
 ## Initial Requirements
@@ -38,7 +38,7 @@ Allows publishing special variable products where customers upload custom raw im
 - `Studiou_WC_FPP_Upload` - Chunked upload AJAX, file assembly, attachment creation
 - `Studiou_WC_FPP_Single_Product` - Upload area on product page (`woocommerce_single_product_summary` priority 35), WC session storage for file data, validates upload on add-to-cart
 - `Studiou_WC_FPP_Shortcode` - `[studiou_free_photo_product id="X"]` renders product with WC native variation form
-- `Studiou_WC_FPP_Cart` - Reads file data from WC session via `woocommerce_add_cart_item_data`, order item meta, order linking
+- `Studiou_WC_FPP_Cart` - Reads file data from WC session via `woocommerce_add_cart_item_data`, replaces cart/order item thumbnails with uploaded photo, order item meta, order linking
 - `Studiou_WC_FPP_Admin` - Summary page, status management, ZIP download
 
 ## Frontend Flow
@@ -46,10 +46,13 @@ Allows publishing special variable products where customers upload custom raw im
 1. Upload area rendered below variation table via `woocommerce_single_product_summary` (priority 35)
 2. Customer uploads photo via chunked AJAX
 3. On upload complete, JS calls `studiou_wcfpp_set_upload_session` to store file ref in WC session
-4. Customer selects variant and clicks any Add to Cart button (works with any theme/layout)
-5. `woocommerce_add_to_cart_validation` checks WC session for uploaded file
-6. `woocommerce_add_cart_item_data` reads file data from WC session, clears session after use
-7. On checkout, file record is linked to order via `woocommerce_checkout_order_processed`
+4. Product gallery image replaced with uploaded photo preview; restored on remove
+5. JS intercepts all add-to-cart buttons/links/forms — blocks if no photo uploaded
+6. Customer selects variant and clicks any Add to Cart button (works with any theme/layout)
+7. `woocommerce_add_to_cart_validation` checks WC session for uploaded file
+8. `woocommerce_add_cart_item_data` reads file data from WC session, clears session after use
+9. Cart/order item thumbnails replaced with uploaded photo (`woocommerce_cart_item_thumbnail`, `woocommerce_admin_order_item_thumbnail`)
+10. On checkout, file record is linked to order via `woocommerce_checkout_order_processed`
 
 ## Coding Conventions
 

+ 124 - 0
studiou-wc-free-photo-product/assets/js/frontend.js

@@ -14,6 +14,7 @@
         var uploadedAttachmentId = null;
         var uploadedFileRecordId = null;
         var isUploading = false;
+        var originalGalleryImages = {}; // Store original src/srcset to restore on remove
 
         // Upload area elements
         var $dropzone = $uploadWrap.find('#studiou-fpp-dropzone');
@@ -27,6 +28,45 @@
         var $removeBtn = $uploadWrap.find('.studiou-fpp-remove-file');
         var $messages = $uploadWrap.find('.studiou-fpp-messages');
 
+        // Block all add-to-cart actions until photo is uploaded
+        // Intercept form submissions
+        $(document).on('submit', 'form.cart, form.variations_form', function (e) {
+            if (!uploadedAttachmentId) {
+                e.preventDefault();
+                showMessage(studiouWcfppFront.i18n.uploadFile, 'error');
+                scrollToUpload();
+                return false;
+            }
+        });
+
+        // Intercept add-to-cart button clicks (covers AJAX and non-AJAX buttons)
+        $(document).on('click', '.single_add_to_cart_button, .add_to_cart_button, button[name="add-to-cart"], input[name="add-to-cart"]', function (e) {
+            if (!uploadedAttachmentId) {
+                e.preventDefault();
+                e.stopImmediatePropagation();
+                showMessage(studiouWcfppFront.i18n.uploadFile, 'error');
+                scrollToUpload();
+                return false;
+            }
+        });
+
+        // Intercept add-to-cart links (e.g. ?add-to-cart=ID)
+        $(document).on('click', 'a[href*="add-to-cart"]', function (e) {
+            if (!uploadedAttachmentId) {
+                e.preventDefault();
+                e.stopImmediatePropagation();
+                showMessage(studiouWcfppFront.i18n.uploadFile, 'error');
+                scrollToUpload();
+                return false;
+            }
+        });
+
+        function scrollToUpload() {
+            $('html, body').animate({
+                scrollTop: $uploadWrap.offset().top - 100
+            }, 400);
+        }
+
         // Reset upload after successful WC add-to-cart
         $(document.body).on('added_to_cart', function () {
             resetUpload();
@@ -144,6 +184,7 @@
                                 );
 
                                 showPreview(response.data.thumbnail_url, response.data.file_name);
+                                updateProductGallery(response.data.preview_url || response.data.thumbnail_url);
                                 $progress.hide();
                                 isUploading = false;
                             } else {
@@ -195,6 +236,88 @@
             $fileInput.val('');
         }
 
+        function updateProductGallery(imageUrl) {
+            if (!imageUrl) return;
+
+            // Find the main product image (try common WC / theme selectors)
+            var selectors = [
+                '.woocommerce-product-gallery__image img',
+                '.product .wp-post-image',
+                '.product-image img',
+                '.product-thumbnail img',
+                '.entry-summary img.attachment-woocommerce_thumbnail',
+                '.product-gallery img:first'
+            ];
+
+            var $img = null;
+            for (var i = 0; i < selectors.length; i++) {
+                $img = $(selectors[i]).first();
+                if ($img.length) break;
+            }
+
+            if (!$img || !$img.length) return;
+
+            // Save original attributes for restoration
+            if (!originalGalleryImages.src) {
+                originalGalleryImages.src = $img.attr('src');
+                originalGalleryImages.srcset = $img.attr('srcset') || '';
+                originalGalleryImages.sizes = $img.attr('sizes') || '';
+                // Also save the parent link href if it's a gallery link
+                var $parentLink = $img.closest('a');
+                if ($parentLink.length) {
+                    originalGalleryImages.href = $parentLink.attr('href');
+                }
+            }
+
+            // Replace with uploaded image
+            $img.attr('src', imageUrl);
+            $img.removeAttr('srcset');
+            $img.removeAttr('sizes');
+
+            var $parentLink = $img.closest('a');
+            if ($parentLink.length) {
+                $parentLink.attr('href', imageUrl);
+            }
+        }
+
+        function restoreProductGallery() {
+            if (!originalGalleryImages.src) return;
+
+            var selectors = [
+                '.woocommerce-product-gallery__image img',
+                '.product .wp-post-image',
+                '.product-image img',
+                '.product-thumbnail img',
+                '.entry-summary img.attachment-woocommerce_thumbnail',
+                '.product-gallery img:first'
+            ];
+
+            var $img = null;
+            for (var i = 0; i < selectors.length; i++) {
+                $img = $(selectors[i]).first();
+                if ($img.length) break;
+            }
+
+            if (!$img || !$img.length) return;
+
+            $img.attr('src', originalGalleryImages.src);
+            if (originalGalleryImages.srcset) {
+                $img.attr('srcset', originalGalleryImages.srcset);
+            }
+            if (originalGalleryImages.sizes) {
+                $img.attr('sizes', originalGalleryImages.sizes);
+            }
+
+            if (originalGalleryImages.href) {
+                var $parentLink = $img.closest('a');
+                if ($parentLink.length) {
+                    $parentLink.attr('href', originalGalleryImages.href);
+                }
+            }
+
+            originalGalleryImages = {};
+        }
+
         function showPreview(thumbnailUrl, fileName) {
             if (thumbnailUrl) {
                 $previewImg.attr('src', thumbnailUrl).show();
@@ -228,6 +351,7 @@
             uploadedAttachmentId = null;
             uploadedFileRecordId = null;
             clearSession();
+            restoreProductGallery();
             $preview.hide();
             $previewImg.attr('src', '');
             $previewName.text('');

+ 27 - 0
studiou-wc-free-photo-product/includes/class-studiou-wc-fpp-cart.php

@@ -17,11 +17,17 @@ class Studiou_WC_FPP_Cart {
         // Display uploaded file info in cart
         add_filter('woocommerce_get_item_data', array($this, 'display_cart_item_data'), 10, 2);
 
+        // Replace cart item thumbnail with uploaded image
+        add_filter('woocommerce_cart_item_thumbnail', array($this, 'cart_item_thumbnail'), 10, 3);
+
         // Save file reference to order item
         add_action('woocommerce_checkout_create_order_line_item', array($this, 'save_order_item_meta'), 10, 4);
 
         // Link file records to order after checkout
         add_action('woocommerce_checkout_order_processed', array($this, 'link_files_to_order'), 10, 3);
+
+        // Replace order item thumbnail with uploaded image (admin)
+        add_filter('woocommerce_admin_order_item_thumbnail', array($this, 'admin_order_item_thumbnail'), 10, 3);
     }
 
     public function add_cart_item_data($cart_item_data, $product_id, $variation_id) {
@@ -73,6 +79,16 @@ class Studiou_WC_FPP_Cart {
         return $cart_item_data;
     }
 
+    public function cart_item_thumbnail($thumbnail, $cart_item, $cart_item_key) {
+        if (!empty($cart_item['studiou_fpp_attachment_id'])) {
+            $image = wp_get_attachment_image($cart_item['studiou_fpp_attachment_id'], 'woocommerce_thumbnail');
+            if ($image) {
+                return $image;
+            }
+        }
+        return $thumbnail;
+    }
+
     public function display_cart_item_data($item_data, $cart_item) {
         if (isset($cart_item['studiou_fpp_file_name'])) {
             $item_data[] = array(
@@ -105,4 +121,15 @@ class Studiou_WC_FPP_Cart {
             }
         }
     }
+
+    public function admin_order_item_thumbnail($thumbnail, $item_id, $item) {
+        $attachment_id = $item->get_meta('_studiou_fpp_attachment_id');
+        if ($attachment_id) {
+            $image = wp_get_attachment_image($attachment_id, 'thumbnail');
+            if ($image) {
+                return $image;
+            }
+        }
+        return $thumbnail;
+    }
 }

+ 16 - 6
studiou-wc-free-photo-product/includes/class-studiou-wc-fpp-upload.php

@@ -114,11 +114,12 @@ class Studiou_WC_FPP_Upload {
             }
 
             wp_send_json_success(array(
-                'complete'      => true,
-                'attachment_id' => $result['attachment_id'],
+                'complete'       => true,
+                'attachment_id'  => $result['attachment_id'],
                 'file_record_id' => $result['file_record_id'],
-                'thumbnail_url' => $result['thumbnail_url'],
-                'file_name'     => $result['file_name'],
+                'thumbnail_url'  => $result['thumbnail_url'],
+                'preview_url'    => $result['preview_url'],
+                'file_name'      => $result['file_name'],
             ));
             return;
         }
@@ -228,16 +229,24 @@ class Studiou_WC_FPP_Upload {
             wp_set_object_terms($attachment_id, array((int) $media_cat_id), 'studiou_media_category');
         }
 
-        // Get thumbnail URL
+        // Get thumbnail URL (small - for upload preview)
         $thumbnail_url = '';
         $image_src = wp_get_attachment_image_src($attachment_id, 'thumbnail');
         if ($image_src) {
             $thumbnail_url = $image_src[0];
         } else {
-            // Fallback for non-image files (RAW formats)
             $thumbnail_url = wp_mime_type_icon($attachment_id);
         }
 
+        // Get preview URL (larger - for product gallery replacement)
+        $preview_url = '';
+        $preview_src = wp_get_attachment_image_src($attachment_id, 'woocommerce_single');
+        if ($preview_src) {
+            $preview_url = $preview_src[0];
+        } elseif ($image_src) {
+            $preview_url = $image_src[0];
+        }
+
         // Get session key for guests
         $session_key = '';
         if (!is_user_logged_in()) {
@@ -264,6 +273,7 @@ class Studiou_WC_FPP_Upload {
             'attachment_id'  => $attachment_id,
             'file_record_id' => $file_record_id,
             'thumbnail_url'  => $thumbnail_url,
+            'preview_url'    => $preview_url,
             'file_name'      => $file_name,
         );
     }

BIN
studiou-wc-free-photo-product/languages/studiou-wc-free-photo-product-cs_CZ.mo


+ 1 - 1
studiou-wc-free-photo-product/languages/studiou-wc-free-photo-product-cs_CZ.po

@@ -3,7 +3,7 @@
 # This file is distributed under the GPL v2 or later.
 msgid ""
 msgstr ""
-"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.0.4\n"
+"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.0.6\n"
 "Report-Msgid-Bugs-To: https://www.quadarax.com\n"
 "POT-Creation-Date: 2026-04-02 00:00+0000\n"
 "PO-Revision-Date: 2026-04-02 00:00+0000\n"

+ 1 - 5
studiou-wc-free-photo-product/languages/studiou-wc-free-photo-product.pot

@@ -2,7 +2,7 @@
 # This file is distributed under the GPL v2 or later.
 msgid ""
 msgstr ""
-"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.0.4\n"
+"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.0.6\n"
 "Report-Msgid-Bugs-To: https://www.quadarax.com\n"
 "POT-Creation-Date: 2026-04-02 00:00+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
@@ -266,10 +266,6 @@ msgstr ""
 msgid "Invalid file data."
 msgstr ""
 
-#: includes/class-studiou-wc-fpp-admin.php
-msgid "No files selected."
-msgstr ""
-
 #: includes/class-studiou-wc-fpp-cart.php
 msgid "Uploaded Photo"
 msgstr ""

+ 9 - 4
studiou-wc-free-photo-product/readme.md

@@ -32,10 +32,11 @@ WordPress (6.9.4+) / WooCommerce (10.6.2+) plugin that allows publishing special
 When a variable product has Free Photo enabled, the upload area automatically appears on the WooCommerce single product page **below** the product summary (below the variation table and Add to Cart buttons). The customer workflow is:
 
 1. Upload a photo file via drag-and-drop or file browser (below the variation table)
-2. Select a variant from the product variation table
-3. Click the variant's **Add to Cart** button
+2. The product gallery image updates to show the uploaded photo preview
+3. Select a variant from the product variation table
+4. Click the variant's **Add to Cart** button
 
-The uploaded file reference is stored in the WooCommerce session, so it works with any add-to-cart method (form POST, AJAX buttons, individual variation buttons). The plugin validates that a photo is uploaded before allowing add-to-cart. The upload area is rendered via `woocommerce_single_product_summary` hook (priority 35, after the add-to-cart section).
+The uploaded file reference is stored in the WooCommerce session, so it works with any add-to-cart method (form POST, AJAX buttons, individual variation buttons). The plugin validates that a photo is uploaded before allowing add-to-cart — both server-side (`woocommerce_add_to_cart_validation`) and client-side (JS intercepts all add-to-cart buttons/links/forms). The upload area is rendered via `woocommerce_single_product_summary` hook (priority 35, after the add-to-cart section). When the photo is removed, the original product image is restored.
 
 ### Shortcode
 
@@ -86,9 +87,11 @@ Navigate to **Products > Free Photo Files** to manage uploaded files.
 The plugin integrates with WooCommerce's native add-to-cart flow using WC session storage. When a photo is uploaded, the file reference is stored in the WC session via AJAX. When any add-to-cart button is clicked, `woocommerce_add_cart_item_data` reads the file data from the session and attaches it to the cart item. The session data is cleared after use. The `woocommerce_add_to_cart_validation` filter ensures a photo is uploaded before the item can be added. This approach works with any theme or plugin that handles variable product display (individual variation buttons, dropdowns, tables, etc.).
 
 When a customer adds a photo product to the cart:
+- The cart item thumbnail is replaced with the uploaded photo image
 - The uploaded file name is shown as "Uploaded Photo" in the cart
 - On checkout, the file record is linked to the order and order item
-- In the order admin, the uploaded photo file name appears as order item metadata
+- In the order admin, order item thumbnails show the uploaded photo instead of the product image
+- The uploaded photo file name appears as order item metadata
 - Files linked to orders cannot be removed by the customer
 
 ## Media Categories
@@ -198,9 +201,11 @@ studiou-wc-free-photo-product/
 | `woocommerce_single_product_summary` (priority 35) | Action | Renders upload area below the variation table |
 | `woocommerce_add_to_cart_validation` | Filter | Validates photo is uploaded |
 | `woocommerce_add_cart_item_data` | Filter | Captures file data into cart item |
+| `woocommerce_cart_item_thumbnail` | Filter | Replaces cart item thumbnail with uploaded photo |
 | `woocommerce_get_item_data` | Filter | Displays file name in cart |
 | `woocommerce_checkout_create_order_line_item` | Action | Saves file meta to order item |
 | `woocommerce_checkout_order_processed` | Action | Links file record to order |
+| `woocommerce_admin_order_item_thumbnail` | Filter | Replaces order item thumbnail with uploaded photo |
 | `woocommerce_product_data_tabs` | Filter | Adds Free Photo product tab |
 | `woocommerce_product_data_panels` | Action | Renders Free Photo settings panel |
 | `woocommerce_process_product_meta` | Action | Saves Free Photo product meta |

+ 2 - 2
studiou-wc-free-photo-product/studiou-wc-free-photo-product.php

@@ -3,7 +3,7 @@
  * Plugin Name: QDR - Studiou WC Free Photo Product
  * Plugin URI: https://www.quadarax.com/plugins/studiou-wc-free-photo-product
  * Description: Allows publishing special WooCommerce products with variants and custom raw image upload for photo printing
- * Version: 1.0.4
+ * Version: 1.0.6
  * Requires at least: 6.9.4
  * Requires PHP: 8.2
  * Author: Dalibor Votruba
@@ -21,7 +21,7 @@ if (!defined('WPINC')) {
     die;
 }
 
-if (!defined('STUDIOU_WCFPP_VERSION')) define('STUDIOU_WCFPP_VERSION', '1.0.4');
+if (!defined('STUDIOU_WCFPP_VERSION')) define('STUDIOU_WCFPP_VERSION', '1.0.6');
 if (!defined('STUDIOU_WCFPP_PLUGIN_DIR')) define('STUDIOU_WCFPP_PLUGIN_DIR', plugin_dir_path(__FILE__));
 if (!defined('STUDIOU_WCFPP_PLUGIN_URL')) define('STUDIOU_WCFPP_PLUGIN_URL', plugin_dir_url(__FILE__));
 if (!defined('STUDIOU_WCFPP_PLUGIN_BASENAME')) define('STUDIOU_WCFPP_PLUGIN_BASENAME', plugin_basename(__FILE__));