Przeglądaj źródła

Use WC session for upload data, fix assembly freeze — v1.0.0 -> v1.0.4

- Move upload area to woocommerce_single_product_summary (priority 35)
  so it works with any theme/variation layout
- Use WC session instead of hidden form fields to pass file data
  between upload and add-to-cart (theme-agnostic)
- Remove custom variant selector, use WC native variation table
- Fix upload freeze at 90%: wrap wp_generate_attachment_metadata in
  output buffer + try/catch, set_time_limit(120), suppress warnings
- Show "Processing..." while server assembles chunks
- Update shortcode to render WC native variation form
- Update translations (EN/CZ), documentation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dalibor Votruba 3 miesięcy temu
rodzic
commit
bc3fc81d86

+ 18 - 5
studiou-wc-free-photo-product/CLAUDE.md

@@ -2,12 +2,12 @@
 
 ## Project Overview
 
-WordPress/WooCommerce plugin called **studiou-wc-free-photo-product** (QDR - Studiou WC Free Photo Product) v1.0.0.
+WordPress/WooCommerce plugin called **studiou-wc-free-photo-product** (QDR - Studiou WC Free Photo Product) v1.0.3.
 Allows publishing special variable products where customers upload custom raw images to be printed for a price.
 
 ## Initial Requirements
 
-1. WP (6.9.4) / WooCommerce (10.6.2) plugin with variants and custom raw image upload for photo printing. Frontend shows product with upload box, preview, and variant table.
+1. WP (6.9.4) / WooCommerce (10.6.2) plugin with variants and custom raw image upload for photo printing. Frontend shows product with upload box and preview on the WooCommerce product detail page.
 2. Product configuration area: Media Category for uploaded files, Maximum raw file size. Shortcode to embed product on any page.
 3. Uploaded raw files stored in WP media library with a specific media category per product. Chunked upload to bypass PHP upload size limits. Media files linked with product.
 4. Admin summary page: list of uploaded files, download as ZIP (bulk), order number, delete, change state (downloaded, ready, solved).
@@ -17,7 +17,8 @@ Allows publishing special variable products where customers upload custom raw im
 
 - PHP 8.2+, WordPress 6.9.4+, WooCommerce 10.0+
 - jQuery for frontend/admin JS
-- WordPress AJAX (admin-ajax.php) for all async operations
+- WordPress AJAX (admin-ajax.php) for chunked upload
+- WC session storage for passing file data between upload and add-to-cart (theme-agnostic)
 - Custom DB table via dbDelta for file tracking
 - Custom taxonomy `studiou_media_category` on `attachment` post type
 
@@ -35,10 +36,21 @@ Allows publishing special variable products where customers upload custom raw im
 - `Studiou_WC_FPP_DB` - Custom table CRUD, media category taxonomy
 - `Studiou_WC_FPP_Product` - WC product data tab, product meta
 - `Studiou_WC_FPP_Upload` - Chunked upload AJAX, file assembly, attachment creation
-- `Studiou_WC_FPP_Shortcode` - `[studiou_free_photo_product id="X"]`
-- `Studiou_WC_FPP_Cart` - Cart item data, order item meta, order linking
+- `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_Admin` - Summary page, status management, ZIP download
 
+## Frontend Flow
+
+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`
+
 ## Coding Conventions
 
 - Follow patterns from sibling plugin `studiou-wc-product-cat-manage`
@@ -54,6 +66,7 @@ Allows publishing special variable products where customers upload custom raw im
 
 - Generate .mo from .po: `php languages/generate-mo.php` or `wp i18n make-mo languages/`
 - Plugin creates DB table on activation via `register_activation_hook`
+- Activation hook requires explicit `require_once` for DB class (runs before `plugins_loaded`)
 - Temp chunks stored in `wp-content/uploads/studiou-fpp-chunks/` (cleaned on deactivation)
 - Assembled files stored in `wp-content/uploads/YYYY/MM/free-photo/`
 

+ 75 - 108
studiou-wc-free-photo-product/assets/js/frontend.js

@@ -2,45 +2,34 @@
     'use strict';
 
     $(document).ready(function () {
-        var $container = $('.studiou-fpp-product');
-        if (!$container.length) {
+
+        var $uploadWrap = $('.studiou-fpp-upload-wrap');
+        if (!$uploadWrap.length) {
             return;
         }
 
-        var productId = $container.data('product-id');
-        var maxFileSize = parseInt($container.data('max-file-size'), 10) || 50;
+        var productId = $uploadWrap.data('product-id');
+        var maxFileSize = parseInt($uploadWrap.data('max-file-size'), 10) || 50;
         var chunkSize = parseInt(studiouWcfppFront.chunkSize, 10) || 1048576;
-        var selectedVariation = null;
         var uploadedAttachmentId = null;
         var uploadedFileRecordId = null;
         var isUploading = false;
 
-        // Elements
-        var $dropzone = $('#studiou-fpp-dropzone');
-        var $fileInput = $('#studiou-fpp-file-input');
-        var $progress = $container.find('.studiou-fpp-progress');
-        var $progressFill = $container.find('.studiou-fpp-progress-fill');
-        var $progressText = $container.find('.studiou-fpp-progress-text');
-        var $preview = $container.find('.studiou-fpp-preview');
+        // Upload area elements
+        var $dropzone = $uploadWrap.find('#studiou-fpp-dropzone');
+        var $fileInput = $uploadWrap.find('#studiou-fpp-file-input');
+        var $progress = $uploadWrap.find('.studiou-fpp-progress');
+        var $progressFill = $uploadWrap.find('.studiou-fpp-progress-fill');
+        var $progressText = $uploadWrap.find('.studiou-fpp-progress-text');
+        var $preview = $uploadWrap.find('.studiou-fpp-preview');
         var $previewImg = $preview.find('img');
-        var $previewName = $container.find('.studiou-fpp-preview-name');
-        var $removeBtn = $container.find('.studiou-fpp-remove-file');
-        var $addToCartBtn = $container.find('.studiou-fpp-add-to-cart-btn');
-        var $messages = $container.find('.studiou-fpp-messages');
-        var $attachmentInput = $('#studiou-fpp-attachment-id');
-        var $fileRecordInput = $('#studiou-fpp-file-record-id');
-
-        // Variant selection
-        $container.on('change', 'input[name="studiou_fpp_variation"]', function () {
-            selectedVariation = $(this).val();
-            $(this).closest('tbody').find('tr').removeClass('studiou-fpp-variant-selected');
-            $(this).closest('tr').addClass('studiou-fpp-variant-selected');
-            updateAddToCartState();
-        });
+        var $previewName = $uploadWrap.find('.studiou-fpp-preview-name');
+        var $removeBtn = $uploadWrap.find('.studiou-fpp-remove-file');
+        var $messages = $uploadWrap.find('.studiou-fpp-messages');
 
-        // Variant row click
-        $container.on('click', '.studiou-fpp-variant-row td:not(.studiou-fpp-col-select)', function () {
-            $(this).closest('tr').find('input[type="radio"]').prop('checked', true).trigger('change');
+        // Reset upload after successful WC add-to-cart
+        $(document.body).on('added_to_cart', function () {
+            resetUpload();
         });
 
         // Drag and drop
@@ -63,7 +52,7 @@
             }
         });
 
-        $dropzone.on('click', function (e) {
+        $dropzone.on('click', function () {
             if (!isUploading) {
                 $fileInput.trigger('click');
             }
@@ -80,17 +69,11 @@
             removeUploadedFile();
         });
 
-        // Add to cart
-        $addToCartBtn.on('click', function () {
-            addToCart();
-        });
-
         function handleFile(file) {
             if (isUploading) {
                 return;
             }
 
-            // Validate file size
             var maxBytes = maxFileSize * 1024 * 1024;
             if (file.size > maxBytes) {
                 showMessage(studiouWcfppFront.i18n.fileTooLarge.replace('%s', maxFileSize), 'error');
@@ -109,7 +92,6 @@
 
             $dropzone.hide();
             $progress.show();
-            $addToCartBtn.prop('disabled', true);
 
             function uploadNextChunk() {
                 var start = currentChunk * chunkSize;
@@ -127,32 +109,44 @@
                 formData.append('file_size', file.size);
                 formData.append('chunk', blob, 'chunk');
 
+                // Last chunk triggers server-side assembly - needs longer timeout
+                var isLastChunk = (currentChunk === totalChunks - 1);
+                if (isLastChunk) {
+                    $progressFill.css('width', '100%');
+                    $progressText.text(studiouWcfppFront.i18n.processing || 'Processing...');
+                }
+
                 $.ajax({
                     url: studiouWcfppFront.ajaxUrl,
                     type: 'POST',
                     data: formData,
                     processData: false,
                     contentType: false,
+                    timeout: isLastChunk ? 120000 : 30000,
                     success: function (response) {
                         if (response.success) {
-                            currentChunk++;
-                            var percent = Math.round((currentChunk / totalChunks) * 100);
-                            $progressFill.css('width', percent + '%');
-                            $progressText.text(percent + '%');
+                            if (!isLastChunk) {
+                                currentChunk++;
+                                var percent = Math.round((currentChunk / totalChunks) * 100);
+                                $progressFill.css('width', percent + '%');
+                                $progressText.text(percent + '%');
+                            }
 
                             if (response.data.complete) {
-                                // Upload complete
                                 uploadedAttachmentId = response.data.attachment_id;
                                 uploadedFileRecordId = response.data.file_record_id;
-                                $attachmentInput.val(uploadedAttachmentId);
-                                $fileRecordInput.val(uploadedFileRecordId);
+
+                                // Store in WC session so any add-to-cart button picks it up
+                                storeInSession(
+                                    response.data.attachment_id,
+                                    response.data.file_record_id,
+                                    response.data.file_name
+                                );
 
                                 showPreview(response.data.thumbnail_url, response.data.file_name);
                                 $progress.hide();
                                 isUploading = false;
-                                updateAddToCartState();
                             } else {
-                                // Upload next chunk
                                 uploadNextChunk();
                             }
                         } else {
@@ -168,6 +162,31 @@
             uploadNextChunk();
         }
 
+        function storeInSession(attachmentId, fileRecordId, fileName) {
+            $.ajax({
+                url: studiouWcfppFront.ajaxUrl,
+                type: 'POST',
+                data: {
+                    action: 'studiou_wcfpp_set_upload_session',
+                    nonce: studiouWcfppFront.nonce,
+                    attachment_id: attachmentId,
+                    file_record_id: fileRecordId,
+                    file_name: fileName
+                }
+            });
+        }
+
+        function clearSession() {
+            $.ajax({
+                url: studiouWcfppFront.ajaxUrl,
+                type: 'POST',
+                data: {
+                    action: 'studiou_wcfpp_clear_upload_session',
+                    nonce: studiouWcfppFront.nonce
+                }
+            });
+        }
+
         function uploadFailed(message) {
             isUploading = false;
             $progress.hide();
@@ -200,73 +219,21 @@
                     file_record_id: uploadedFileRecordId
                 },
                 success: function () {
-                    uploadedAttachmentId = null;
-                    uploadedFileRecordId = null;
-                    $attachmentInput.val('');
-                    $fileRecordInput.val('');
-                    $preview.hide();
-                    $previewImg.attr('src', '');
-                    $previewName.text('');
-                    $dropzone.show();
-                    $fileInput.val('');
-                    updateAddToCartState();
+                    resetUpload();
                 }
             });
         }
 
-        function addToCart() {
-            if (!selectedVariation) {
-                showMessage(studiouWcfppFront.i18n.selectVariant, 'error');
-                return;
-            }
-            if (!uploadedAttachmentId) {
-                showMessage(studiouWcfppFront.i18n.uploadFile, 'error');
-                return;
-            }
-
-            $addToCartBtn.prop('disabled', true);
+        function resetUpload() {
+            uploadedAttachmentId = null;
+            uploadedFileRecordId = null;
+            clearSession();
+            $preview.hide();
+            $previewImg.attr('src', '');
+            $previewName.text('');
+            $dropzone.show();
+            $fileInput.val('');
             clearMessages();
-
-            $.ajax({
-                url: studiouWcfppFront.ajaxUrl,
-                type: 'POST',
-                data: {
-                    action: 'studiou_wcfpp_add_to_cart',
-                    nonce: studiouWcfppFront.nonce,
-                    product_id: productId,
-                    variation_id: selectedVariation,
-                    attachment_id: uploadedAttachmentId,
-                    file_record_id: uploadedFileRecordId
-                },
-                success: function (response) {
-                    if (response.success) {
-                        showMessage(studiouWcfppFront.i18n.addedToCart, 'success');
-                        // Reset upload state for next photo
-                        uploadedAttachmentId = null;
-                        uploadedFileRecordId = null;
-                        $attachmentInput.val('');
-                        $fileRecordInput.val('');
-                        $preview.hide();
-                        $dropzone.show();
-                        $fileInput.val('');
-                        updateAddToCartState();
-
-                        // Update WC cart fragments
-                        $(document.body).trigger('wc_fragment_refresh');
-                    } else {
-                        showMessage(response.data ? response.data.message : studiouWcfppFront.i18n.addToCartError, 'error');
-                        updateAddToCartState();
-                    }
-                },
-                error: function () {
-                    showMessage(studiouWcfppFront.i18n.addToCartError, 'error');
-                    updateAddToCartState();
-                }
-            });
-        }
-
-        function updateAddToCartState() {
-            $addToCartBtn.prop('disabled', !(selectedVariation && uploadedAttachmentId));
         }
 
         function showMessage(text, type) {

+ 33 - 65
studiou-wc-free-photo-product/includes/class-studiou-wc-fpp-cart.php

@@ -11,9 +11,8 @@ class Studiou_WC_FPP_Cart {
     public function __construct($db) {
         $this->db = $db;
 
-        // AJAX add to cart
-        add_action('wp_ajax_studiou_wcfpp_add_to_cart', array($this, 'ajax_add_to_cart'));
-        add_action('wp_ajax_nopriv_studiou_wcfpp_add_to_cart', array($this, 'ajax_add_to_cart'));
+        // Capture file data from WC session on add-to-cart
+        add_filter('woocommerce_add_cart_item_data', array($this, 'add_cart_item_data'), 10, 3);
 
         // Display uploaded file info in cart
         add_filter('woocommerce_get_item_data', array($this, 'display_cart_item_data'), 10, 2);
@@ -23,84 +22,54 @@ class Studiou_WC_FPP_Cart {
 
         // Link file records to order after checkout
         add_action('woocommerce_checkout_order_processed', array($this, 'link_files_to_order'), 10, 3);
-
-        // Ensure cart item data persists
-        add_filter('woocommerce_add_cart_item_data', array($this, 'preserve_cart_item_data'), 10, 3);
     }
 
-    public function ajax_add_to_cart() {
-        check_ajax_referer('studiou-wcfpp-front-nonce', 'nonce');
-
-        $product_id    = isset($_POST['product_id']) ? absint($_POST['product_id']) : 0;
-        $variation_id  = isset($_POST['variation_id']) ? absint($_POST['variation_id']) : 0;
-        $attachment_id = isset($_POST['attachment_id']) ? absint($_POST['attachment_id']) : 0;
-        $file_record_id = isset($_POST['file_record_id']) ? absint($_POST['file_record_id']) : 0;
-
-        if (!$product_id || !$variation_id || !$attachment_id || !$file_record_id) {
-            wp_send_json_error(array('message' => __('Missing required data.', 'studiou-wc-free-photo-product')));
-            return;
-        }
-
-        // Verify product is FPP enabled
+    public function add_cart_item_data($cart_item_data, $product_id, $variation_id) {
+        // Check if this product (or its parent) is FPP enabled
+        $fpp_product_id = $product_id;
         if (!Studiou_WC_FPP_Product::is_fpp_product($product_id)) {
-            wp_send_json_error(array('message' => __('Invalid product.', 'studiou-wc-free-photo-product')));
-            return;
+            $product = wc_get_product($product_id);
+            if ($product && $product->get_parent_id() && Studiou_WC_FPP_Product::is_fpp_product($product->get_parent_id())) {
+                $fpp_product_id = $product->get_parent_id();
+            } else {
+                return $cart_item_data;
+            }
         }
 
-        // Verify the file record exists and belongs to this user
-        $record = $this->db->get_file_record($file_record_id);
-        if (!$record || (int) $record->attachment_id !== $attachment_id) {
-            wp_send_json_error(array('message' => __('Invalid file.', 'studiou-wc-free-photo-product')));
-            return;
+        // Read file data from WC session
+        if (!WC()->session) {
+            return $cart_item_data;
         }
 
-        // Verify variation belongs to product
-        $variation = wc_get_product($variation_id);
-        if (!$variation || $variation->get_parent_id() !== $product_id) {
-            wp_send_json_error(array('message' => __('Invalid variant.', 'studiou-wc-free-photo-product')));
-            return;
+        $attachment_id = WC()->session->get('studiou_fpp_attachment_id');
+        $file_record_id = WC()->session->get('studiou_fpp_file_record_id');
+        $file_name = WC()->session->get('studiou_fpp_file_name');
+
+        if (!$attachment_id || !$file_record_id) {
+            return $cart_item_data;
         }
 
-        // Get variation attributes for cart
-        $variation_data = array();
-        $attributes = $variation->get_variation_attributes();
-        foreach ($attributes as $key => $value) {
-            $variation_data[$key] = $value;
+        // Verify the file record exists
+        $record = $this->db->get_file_record($file_record_id);
+        if (!$record || (int) $record->attachment_id !== (int) $attachment_id) {
+            return $cart_item_data;
         }
 
-        // Custom cart item data
-        $cart_item_data = array(
-            'studiou_fpp_attachment_id'  => $attachment_id,
-            'studiou_fpp_file_record_id' => $file_record_id,
-            'studiou_fpp_file_name'      => $record->file_name,
-        );
+        $cart_item_data['studiou_fpp_attachment_id'] = $attachment_id;
+        $cart_item_data['studiou_fpp_file_record_id'] = $file_record_id;
+        $cart_item_data['studiou_fpp_file_name'] = $file_name ?: $record->file_name;
 
         // Update file record with variation
+        $actual_variation_id = $variation_id ?: $product_id;
         $this->db->update_file_record($file_record_id, array(
-            'variation_id' => $variation_id,
+            'variation_id' => $actual_variation_id,
         ));
 
-        // Add to WC cart
-        $cart_item_key = WC()->cart->add_to_cart(
-            $product_id,
-            1,
-            $variation_id,
-            $variation_data,
-            $cart_item_data
-        );
-
-        if ($cart_item_key) {
-            wp_send_json_success(array(
-                'message'        => __('Product added to cart.', 'studiou-wc-free-photo-product'),
-                'cart_item_key'  => $cart_item_key,
-            ));
-        } else {
-            wp_send_json_error(array('message' => __('Could not add to cart.', 'studiou-wc-free-photo-product')));
-        }
-    }
+        // Clear session data after reading (so next add-to-cart is clean)
+        WC()->session->set('studiou_fpp_attachment_id', null);
+        WC()->session->set('studiou_fpp_file_record_id', null);
+        WC()->session->set('studiou_fpp_file_name', null);
 
-    public function preserve_cart_item_data($cart_item_data, $product_id, $variation_id) {
-        // Cart item data is already set by add_to_cart call above
         return $cart_item_data;
     }
 
@@ -124,7 +93,6 @@ class Studiou_WC_FPP_Cart {
         }
         if (isset($values['studiou_fpp_file_name'])) {
             $item->add_meta_data('_studiou_fpp_file_name', $values['studiou_fpp_file_name']);
-            // Also add a visible meta for admin display
             $item->add_meta_data(__('Uploaded Photo', 'studiou-wc-free-photo-product'), $values['studiou_fpp_file_name']);
         }
     }

+ 17 - 4
studiou-wc-free-photo-product/includes/class-studiou-wc-fpp-shortcode.php

@@ -36,13 +36,26 @@ class Studiou_WC_FPP_Shortcode {
         wp_enqueue_style('studiou-wcfpp-frontend');
         wp_enqueue_script('studiou-wcfpp-frontend');
 
-        // Get variations
-        $variations = $product->get_available_variations();
-        $attributes = $product->get_variation_attributes();
         $max_file_size = Studiou_WC_FPP_Product::get_product_max_file_size($product_id);
 
+        // Set global product so WC template functions work
+        global $post;
+        $original_post = $post;
+        $post = get_post($product_id);
+        setup_postdata($post);
+
         ob_start();
         include STUDIOU_WCFPP_PLUGIN_DIR . 'views/frontend-product.php';
-        return ob_get_clean();
+        $output = ob_get_clean();
+
+        // Restore original post
+        $post = $original_post;
+        if ($original_post) {
+            setup_postdata($original_post);
+        } else {
+            wp_reset_postdata();
+        }
+
+        return $output;
     }
 }

+ 93 - 0
studiou-wc-free-photo-product/includes/class-studiou-wc-fpp-single-product.php

@@ -0,0 +1,93 @@
+<?php
+if (!defined('WPINC')) {
+    die;
+}
+
+class Studiou_WC_FPP_Single_Product {
+
+    public function __construct() {
+        // Render upload area below the variation table on single product page
+        add_action('woocommerce_single_product_summary', array($this, 'render_upload_area'), 35);
+        // Validation before add to cart
+        add_filter('woocommerce_add_to_cart_validation', array($this, 'validate_add_to_cart'), 10, 3);
+        // AJAX: store uploaded file reference in WC session
+        add_action('wp_ajax_studiou_wcfpp_set_upload_session', array($this, 'ajax_set_upload_session'));
+        add_action('wp_ajax_nopriv_studiou_wcfpp_set_upload_session', array($this, 'ajax_set_upload_session'));
+        add_action('wp_ajax_studiou_wcfpp_clear_upload_session', array($this, 'ajax_clear_upload_session'));
+        add_action('wp_ajax_nopriv_studiou_wcfpp_clear_upload_session', array($this, 'ajax_clear_upload_session'));
+    }
+
+    public function render_upload_area() {
+        global $product;
+        if (!$product || !Studiou_WC_FPP_Product::is_fpp_product($product->get_id())) {
+            return;
+        }
+
+        wp_enqueue_style('studiou-wcfpp-frontend');
+        wp_enqueue_script('studiou-wcfpp-frontend');
+
+        $product_id = $product->get_id();
+        $max_file_size = Studiou_WC_FPP_Product::get_product_max_file_size($product_id);
+
+        include STUDIOU_WCFPP_PLUGIN_DIR . 'views/single-product-upload.php';
+    }
+
+    public function validate_add_to_cart($passed, $product_id, $quantity) {
+        if (!Studiou_WC_FPP_Product::is_fpp_product($product_id)) {
+            // Also check parent product for variations
+            $product = wc_get_product($product_id);
+            if ($product && $product->get_parent_id()) {
+                $parent_id = $product->get_parent_id();
+                if (!Studiou_WC_FPP_Product::is_fpp_product($parent_id)) {
+                    return $passed;
+                }
+            } else {
+                return $passed;
+            }
+        }
+
+        // Check WC session for uploaded file data
+        if (WC()->session) {
+            $attachment_id = WC()->session->get('studiou_fpp_attachment_id');
+            if ($attachment_id) {
+                return $passed;
+            }
+        }
+
+        wc_add_notice(__('Please upload a photo before adding to cart.', 'studiou-wc-free-photo-product'), 'error');
+        return false;
+    }
+
+    public function ajax_set_upload_session() {
+        check_ajax_referer('studiou-wcfpp-front-nonce', 'nonce');
+
+        $attachment_id = isset($_POST['attachment_id']) ? absint($_POST['attachment_id']) : 0;
+        $file_record_id = isset($_POST['file_record_id']) ? absint($_POST['file_record_id']) : 0;
+        $file_name = isset($_POST['file_name']) ? sanitize_text_field($_POST['file_name']) : '';
+
+        if (!$attachment_id || !$file_record_id) {
+            wp_send_json_error(array('message' => __('Invalid file data.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        if (WC()->session) {
+            WC()->session->set('studiou_fpp_attachment_id', $attachment_id);
+            WC()->session->set('studiou_fpp_file_record_id', $file_record_id);
+            WC()->session->set('studiou_fpp_file_name', $file_name);
+        }
+
+        wp_send_json_success();
+    }
+
+    public function ajax_clear_upload_session() {
+        check_ajax_referer('studiou-wcfpp-front-nonce', 'nonce');
+
+        if (WC()->session) {
+            WC()->session->set('studiou_fpp_attachment_id', null);
+            WC()->session->set('studiou_fpp_file_record_id', null);
+            WC()->session->set('studiou_fpp_file_name', null);
+        }
+
+        wp_send_json_success();
+    }
+}

+ 25 - 3
studiou-wc-free-photo-product/includes/class-studiou-wc-fpp-upload.php

@@ -96,7 +96,18 @@ class Studiou_WC_FPP_Upload {
 
         // If this is the last chunk, assemble the file
         if ($chunk_index === $total_chunks - 1) {
+            // Clean any output that may have been generated
+            while (ob_get_level()) {
+                ob_end_clean();
+            }
+
             $result = $this->assemble_chunks($upload_id, $total_chunks, $file_name, $product_id);
+
+            // Clean again before sending JSON
+            while (ob_get_level()) {
+                ob_end_clean();
+            }
+
             if (is_wp_error($result)) {
                 wp_send_json_error(array('message' => $result->get_error_message()));
                 return;
@@ -195,10 +206,21 @@ class Studiou_WC_FPP_Upload {
             return $attachment_id;
         }
 
-        // Generate metadata
+        // Generate metadata (wrapped in output buffer to prevent stray output corrupting JSON)
         require_once(ABSPATH . 'wp-admin/includes/image.php');
-        $metadata = wp_generate_attachment_metadata($attachment_id, $assembled_path);
-        wp_update_attachment_metadata($attachment_id, $metadata);
+        ob_start();
+        try {
+            @set_time_limit(120);
+            $metadata = @wp_generate_attachment_metadata($attachment_id, $assembled_path);
+            if (!empty($metadata)) {
+                wp_update_attachment_metadata($attachment_id, $metadata);
+            }
+        } catch (\Throwable $e) {
+            if (defined('WP_DEBUG') && WP_DEBUG) {
+                error_log('STUDIOU FPP: metadata generation failed - ' . $e->getMessage());
+            }
+        }
+        ob_end_clean();
 
         // Assign media category
         $media_cat_id = Studiou_WC_FPP_Product::get_product_media_category($product_id);

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


+ 8 - 20
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.0\n"
+"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.0.4\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"
@@ -49,6 +49,9 @@ msgstr "Nahrávání..."
 msgid "Upload complete"
 msgstr "Nahrávání dokončeno"
 
+msgid "Processing..."
+msgstr "Zpracování..."
+
 msgid "Upload failed. Please try again."
 msgstr "Nahrávání selhalo. Zkuste to prosím znovu."
 
@@ -196,27 +199,15 @@ msgstr "Tento produkt nepodporuje nahrávání fotografií."
 msgid "This product must be a variable product."
 msgstr "Tento produkt musí být variabilní produkt."
 
-msgid "Missing required data."
-msgstr "Chybějící povinné údaje."
-
-msgid "Invalid variant."
-msgstr "Neplatná varianta."
-
-msgid "Product added to cart."
-msgstr "Produkt přidán do košíku."
+msgid "Please upload a photo before adding to cart."
+msgstr "Před přidáním do košíku prosím nahrajte fotografii."
 
-msgid "Could not add to cart."
-msgstr "Nelze přidat do košíku."
+msgid "Invalid file data."
+msgstr "Neplatná data souboru."
 
 msgid "Uploaded Photo"
 msgstr "Nahraná fotografie"
 
-msgid "Select Variant"
-msgstr "Vyberte variantu"
-
-msgid "Price"
-msgstr "Cena"
-
 msgid "Upload Your Photo"
 msgstr "Nahrajte svou fotografii"
 
@@ -226,9 +217,6 @@ msgstr "Maximální velikost souboru: %s MB. Povolené formáty: JPEG, PNG, TIFF
 msgid "Preview"
 msgstr "Náhled"
 
-msgid "Add to Cart"
-msgstr "Přidat do košíku"
-
 msgid "All Statuses"
 msgstr "Všechny stavy"
 

+ 14 - 34
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.0\n"
+"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.0.4\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"
@@ -58,6 +58,10 @@ msgstr ""
 msgid "Upload complete"
 msgstr ""
 
+#: studiou-wc-free-photo-product.php
+msgid "Processing..."
+msgstr ""
+
 #: studiou-wc-free-photo-product.php
 msgid "Upload failed. Please try again."
 msgstr ""
@@ -254,50 +258,34 @@ msgstr ""
 msgid "This product must be a variable product."
 msgstr ""
 
-#: includes/class-studiou-wc-fpp-cart.php
-msgid "Missing required data."
+#: includes/class-studiou-wc-fpp-single-product.php
+msgid "Please upload a photo before adding to cart."
 msgstr ""
 
-#: includes/class-studiou-wc-fpp-cart.php
-msgid "Invalid variant."
+#: includes/class-studiou-wc-fpp-single-product.php
+msgid "Invalid file data."
 msgstr ""
 
-#: includes/class-studiou-wc-fpp-cart.php
-msgid "Product added to cart."
-msgstr ""
-
-#: includes/class-studiou-wc-fpp-cart.php
-msgid "Could not add to cart."
+#: includes/class-studiou-wc-fpp-admin.php
+msgid "No files selected."
 msgstr ""
 
 #: includes/class-studiou-wc-fpp-cart.php
 msgid "Uploaded Photo"
 msgstr ""
 
-#: views/frontend-product.php
-msgid "Select Variant"
-msgstr ""
-
-#: views/frontend-product.php
-msgid "Price"
-msgstr ""
-
-#: views/frontend-product.php
+#: views/single-product-upload.php
 msgid "Upload Your Photo"
 msgstr ""
 
-#: views/frontend-product.php
+#: views/single-product-upload.php
 msgid "Maximum file size: %s MB. Accepted formats: JPEG, PNG, TIFF, BMP, PSD, RAW (CR2, NEF, ARW, DNG, ORF, RW2, RAF), WebP."
 msgstr ""
 
-#: views/frontend-product.php
+#: views/single-product-upload.php
 msgid "Preview"
 msgstr ""
 
-#: views/frontend-product.php
-msgid "Add to Cart"
-msgstr ""
-
 #: views/admin-summary.php
 msgid "All Statuses"
 msgstr ""
@@ -419,10 +407,6 @@ msgstr ""
 msgid "%d file(s) deleted."
 msgstr ""
 
-#: includes/class-studiou-wc-fpp-admin.php
-msgid "No files found."
-msgstr ""
-
 #: includes/class-studiou-wc-fpp-admin.php
 msgid "ZIP extension not available on server."
 msgstr ""
@@ -438,7 +422,3 @@ msgstr ""
 #: includes/class-studiou-wc-fpp-admin.php
 msgid "Security check failed."
 msgstr ""
-
-#: includes/class-studiou-wc-fpp-admin.php
-msgid "File not found."
-msgstr ""

+ 54 - 26
studiou-wc-free-photo-product/readme.md

@@ -1,6 +1,6 @@
 # QDR - Studiou WC Free Photo Product
 
-WordPress (6.9.4+) / WooCommerce (10.6.2+) plugin that allows publishing special variable products with custom raw image upload for photo printing. Customers select a product variant and upload their photo file, which gets stored in the WordPress media library under a configurable media category.
+WordPress (6.9.4+) / WooCommerce (10.6.2+) plugin that allows publishing special variable products with custom raw image upload for photo printing. Customers select a product variant using WooCommerce's native variation selector and upload their photo file, which gets stored in the WordPress media library under a configurable media category.
 
 ## Requirements
 
@@ -21,15 +21,25 @@ WordPress (6.9.4+) / WooCommerce (10.6.2+) plugin that allows publishing special
 
 1. Create or edit a WooCommerce product and set its type to **Variable**
 2. Add variations with attributes (e.g. size: 10x15, 20x30, A4) and prices
-3. Go to the **Free Photo** tab in the product data panel
+3. Go to the **Free Photo** tab in the product data panel (visible for Variable products)
 4. Check **Enable Free Photo**
 5. Select or create a **Media Category** - uploaded files will be assigned to this category
 6. Set **Max File Size (MB)** - maximum allowed upload size per file (1-500 MB, default 50)
-7. Copy the displayed **Shortcode** for use on pages
+7. Copy the displayed **Shortcode** for use on custom pages
 
-### Displaying on Frontend
+### Product Detail Page
 
-Use the shortcode on any page or post:
+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
+
+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).
+
+### Shortcode
+
+The shortcode can be used to embed the product with its variation selector and upload area on any page:
 
 ```
 [studiou_free_photo_product id="123"]
@@ -37,9 +47,8 @@ Use the shortcode on any page or post:
 
 Replace `123` with your product ID. The shortcode renders:
 - Product image, name, and short description
-- Variant selection table with attributes and prices
-- Drag-and-drop upload area with progress bar and preview
-- Add to Cart button (enabled after variant selection + file upload)
+- WooCommerce native variation selector and Add to Cart form
+- Upload area with drag-and-drop, progress bar, and preview (injected into the form)
 
 ### Accepted File Formats
 
@@ -74,6 +83,8 @@ Navigate to **Products > Free Photo Files** to manage uploaded files.
 
 ## Cart and Order Integration
 
+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 uploaded file name is shown as "Uploaded Photo" in the cart
 - On checkout, the file record is linked to the order and order item
@@ -83,7 +94,7 @@ When a customer adds a photo product to the cart:
 ## Media Categories
 
 The plugin registers a custom taxonomy `studiou_media_category` on the `attachment` post type. Categories are visible in the Media Library admin column and can be managed from:
-- **Free Photo** product tab (inline add)
+- **Free Photo** product tab (inline add via "+" button)
 - Media Library sidebar
 
 Each product has one assigned media category. All files uploaded for that product are tagged with it.
@@ -140,26 +151,28 @@ The plugin creates one custom table on activation:
 
 ```
 studiou-wc-free-photo-product/
-├── studiou-wc-free-photo-product.php   Main plugin file
+├── studiou-wc-free-photo-product.php        Main plugin file
 ├── includes/
-│   ├── class-studiou-wc-fpp-db.php     Database + media taxonomy
-│   ├── class-studiou-wc-fpp-product.php Product data tab
-│   ├── class-studiou-wc-fpp-upload.php  Chunked upload handler
-│   ├── class-studiou-wc-fpp-shortcode.php Shortcode renderer
-│   ├── class-studiou-wc-fpp-cart.php    Cart/order integration
-│   └── class-studiou-wc-fpp-admin.php   Admin summary page
+│   ├── class-studiou-wc-fpp-db.php          Database + media taxonomy
+│   ├── class-studiou-wc-fpp-product.php     Product data tab
+│   ├── class-studiou-wc-fpp-upload.php      Chunked upload handler
+│   ├── class-studiou-wc-fpp-shortcode.php   Shortcode renderer
+│   ├── class-studiou-wc-fpp-single-product.php  Product page upload area
+│   ├── class-studiou-wc-fpp-cart.php        Cart/order integration (native WC flow)
+│   └── class-studiou-wc-fpp-admin.php       Admin summary page
 ├── views/
-│   ├── frontend-product.php            Shortcode template
-│   └── admin-summary.php              Admin file list template
+│   ├── frontend-product.php                 Shortcode template
+│   ├── single-product-upload.php            Upload area for product page
+│   └── admin-summary.php                    Admin file list template
 ├── assets/
-│   ├── css/admin.css                   Admin styles
-│   ├── css/frontend.css                Frontend styles
-│   ├── js/admin.js                     Admin JS
-│   └── js/frontend.js                  Frontend JS (chunked upload)
+│   ├── css/admin.css                        Admin styles
+│   ├── css/frontend.css                     Frontend styles
+│   ├── js/admin.js                          Admin JS
+│   └── js/frontend.js                       Frontend JS (chunked upload)
 └── languages/
-    ├── studiou-wc-free-photo-product.pot   Translation template
-    ├── studiou-wc-free-photo-product-cs_CZ.po Czech translation
-    └── generate-mo.php                 MO file generator
+    ├── studiou-wc-free-photo-product.pot    Translation template
+    ├── studiou-wc-free-photo-product-cs_CZ.po  Czech translation
+    └── generate-mo.php                      MO file generator
 ```
 
 ## Hooks and Filters
@@ -170,13 +183,28 @@ studiou-wc-free-photo-product/
 |---|---|---|
 | `studiou_wcfpp_upload_chunk` | Both | Upload a file chunk |
 | `studiou_wcfpp_remove_upload` | Both | Remove an uploaded file (pre-order) |
-| `studiou_wcfpp_add_to_cart` | Both | Add photo product to WC cart |
+| `studiou_wcfpp_set_upload_session` | Both | Store uploaded file reference in WC session |
+| `studiou_wcfpp_clear_upload_session` | Both | Clear uploaded file reference from WC session |
 | `studiou_wcfpp_add_media_category` | Admin | Create a new media category |
 | `studiou_wcfpp_update_status` | Admin | Update single file status |
 | `studiou_wcfpp_bulk_status` | Admin | Bulk update file statuses |
 | `studiou_wcfpp_delete_files` | Admin | Delete files (single or bulk) |
 | `studiou_wcfpp_download_zip` | Admin | Generate ZIP of selected files |
 
+### WooCommerce Hooks Used
+
+| Hook | Type | Description |
+|---|---|---|
+| `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_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_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 |
+
 ### Product Meta Keys
 
 | Key | Description |

+ 9 - 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.0
+ * Version: 1.0.4
  * 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.0');
+if (!defined('STUDIOU_WCFPP_VERSION')) define('STUDIOU_WCFPP_VERSION', '1.0.4');
 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__));
@@ -41,6 +41,9 @@ class Studiou_WC_Free_Photo_Product {
     /** @var Studiou_WC_FPP_Shortcode */
     private $shortcode;
 
+    /** @var Studiou_WC_FPP_Single_Product */
+    private $single_product;
+
     /** @var Studiou_WC_FPP_Cart */
     private $cart;
 
@@ -62,6 +65,7 @@ class Studiou_WC_Free_Photo_Product {
         require_once STUDIOU_WCFPP_PLUGIN_DIR . 'includes/class-studiou-wc-fpp-product.php';
         require_once STUDIOU_WCFPP_PLUGIN_DIR . 'includes/class-studiou-wc-fpp-upload.php';
         require_once STUDIOU_WCFPP_PLUGIN_DIR . 'includes/class-studiou-wc-fpp-shortcode.php';
+        require_once STUDIOU_WCFPP_PLUGIN_DIR . 'includes/class-studiou-wc-fpp-single-product.php';
         require_once STUDIOU_WCFPP_PLUGIN_DIR . 'includes/class-studiou-wc-fpp-cart.php';
         require_once STUDIOU_WCFPP_PLUGIN_DIR . 'includes/class-studiou-wc-fpp-admin.php';
     }
@@ -71,6 +75,7 @@ class Studiou_WC_Free_Photo_Product {
         $this->product = new Studiou_WC_FPP_Product();
         $this->upload = new Studiou_WC_FPP_Upload($this->db);
         $this->shortcode = new Studiou_WC_FPP_Shortcode();
+        $this->single_product = new Studiou_WC_FPP_Single_Product();
         $this->cart = new Studiou_WC_FPP_Cart($this->db);
         $this->admin = new Studiou_WC_FPP_Admin($this->db);
     }
@@ -193,6 +198,7 @@ class Studiou_WC_Free_Photo_Product {
             'i18n' => array(
                 'uploading' => __('Uploading...', 'studiou-wc-free-photo-product'),
                 'uploadComplete' => __('Upload complete', 'studiou-wc-free-photo-product'),
+                'processing' => __('Processing...', 'studiou-wc-free-photo-product'),
                 'uploadError' => __('Upload failed. Please try again.', 'studiou-wc-free-photo-product'),
                 'fileTooLarge' => __('File is too large. Maximum size: %s MB', 'studiou-wc-free-photo-product'),
                 'selectVariant' => __('Please select a variant.', 'studiou-wc-free-photo-product'),
@@ -206,6 +212,7 @@ class Studiou_WC_Free_Photo_Product {
     }
 
     public static function activate() {
+        require_once STUDIOU_WCFPP_PLUGIN_DIR . 'includes/class-studiou-wc-fpp-db.php';
         $db = new Studiou_WC_FPP_DB();
         $db->create_tables();
 

+ 6 - 106
studiou-wc-free-photo-product/views/frontend-product.php

@@ -4,13 +4,10 @@ if (!defined('WPINC')) {
 }
 
 /** @var WC_Product_Variable $product */
-/** @var array $variations */
-/** @var array $attributes */
 /** @var int $max_file_size */
 $product_id = $product->get_id();
-$currency_symbol = get_woocommerce_currency_symbol();
 ?>
-<div class="studiou-fpp-product" data-product-id="<?php echo esc_attr($product_id); ?>" data-max-file-size="<?php echo esc_attr($max_file_size); ?>">
+<div class="studiou-fpp-product">
 
     <div class="studiou-fpp-product-header">
         <?php if ($product->get_image_id()) : ?>
@@ -28,109 +25,12 @@ $currency_symbol = get_woocommerce_currency_symbol();
         </div>
     </div>
 
+    <?php
+    // Render WooCommerce native product form (includes variation selectors + add to cart)
+    // The upload area is injected via woocommerce_before_add_to_cart_button hook
+    ?>
     <div class="studiou-fpp-section">
-        <h3><?php esc_html_e('Select Variant', 'studiou-wc-free-photo-product'); ?></h3>
-        <div class="studiou-fpp-variants-table-wrap">
-            <table class="studiou-fpp-variants-table">
-                <thead>
-                    <tr>
-                        <th class="studiou-fpp-col-select"></th>
-                        <?php
-                        $attr_labels = array();
-                        foreach ($attributes as $attr_name => $attr_values) {
-                            $taxonomy = str_replace('attribute_', '', $attr_name);
-                            if (taxonomy_exists($attr_name)) {
-                                $tax_obj = get_taxonomy($attr_name);
-                                $label = $tax_obj->labels->singular_name;
-                            } else {
-                                $label = wc_attribute_label($attr_name, $product);
-                            }
-                            $attr_labels[$attr_name] = $label;
-                            ?>
-                            <th><?php echo esc_html($label); ?></th>
-                            <?php
-                        }
-                        ?>
-                        <th class="studiou-fpp-col-price"><?php esc_html_e('Price', 'studiou-wc-free-photo-product'); ?></th>
-                    </tr>
-                </thead>
-                <tbody>
-                    <?php foreach ($variations as $index => $variation) :
-                        $var_product = wc_get_product($variation['variation_id']);
-                        if (!$var_product || !$var_product->is_purchasable() || !$var_product->is_in_stock()) {
-                            continue;
-                        }
-                        ?>
-                        <tr class="studiou-fpp-variant-row" data-variation-id="<?php echo esc_attr($variation['variation_id']); ?>">
-                            <td class="studiou-fpp-col-select">
-                                <input type="radio" name="studiou_fpp_variation" value="<?php echo esc_attr($variation['variation_id']); ?>" id="studiou-fpp-var-<?php echo esc_attr($variation['variation_id']); ?>" />
-                            </td>
-                            <?php foreach ($attributes as $attr_name => $attr_values) :
-                                $attr_key = 'attribute_' . sanitize_title($attr_name);
-                                $attr_value = isset($variation['attributes'][$attr_key]) ? $variation['attributes'][$attr_key] : '';
-                                // Get term name if it's a taxonomy
-                                if (taxonomy_exists($attr_name) && !empty($attr_value)) {
-                                    $term = get_term_by('slug', $attr_value, $attr_name);
-                                    $display_value = $term ? $term->name : $attr_value;
-                                } else {
-                                    $display_value = $attr_value;
-                                }
-                                ?>
-                                <td><?php echo esc_html($display_value); ?></td>
-                            <?php endforeach; ?>
-                            <td class="studiou-fpp-col-price">
-                                <?php echo wp_kses_post($var_product->get_price_html()); ?>
-                            </td>
-                        </tr>
-                    <?php endforeach; ?>
-                </tbody>
-            </table>
-        </div>
-    </div>
-
-    <div class="studiou-fpp-section">
-        <h3><?php esc_html_e('Upload Your Photo', 'studiou-wc-free-photo-product'); ?></h3>
-        <p class="studiou-fpp-upload-info">
-            <?php echo esc_html(sprintf(
-                __('Maximum file size: %s MB. Accepted formats: JPEG, PNG, TIFF, BMP, PSD, RAW (CR2, NEF, ARW, DNG, ORF, RW2, RAF), WebP.', 'studiou-wc-free-photo-product'),
-                $max_file_size
-            )); ?>
-        </p>
-
-        <div class="studiou-fpp-upload-area" id="studiou-fpp-dropzone">
-            <div class="studiou-fpp-upload-prompt">
-                <span class="studiou-fpp-upload-icon dashicons dashicons-upload"></span>
-                <p><?php esc_html_e('Drag & drop your file here or click to browse', 'studiou-wc-free-photo-product'); ?></p>
-            </div>
-            <input type="file" id="studiou-fpp-file-input" class="studiou-fpp-file-input"
-                accept=".jpg,.jpeg,.png,.tiff,.tif,.bmp,.psd,.cr2,.nef,.arw,.dng,.orf,.rw2,.raf,.webp" />
-        </div>
-
-        <div class="studiou-fpp-progress" style="display:none;">
-            <div class="studiou-fpp-progress-bar">
-                <div class="studiou-fpp-progress-fill" style="width:0%"></div>
-            </div>
-            <span class="studiou-fpp-progress-text">0%</span>
-        </div>
-
-        <div class="studiou-fpp-preview" style="display:none;">
-            <div class="studiou-fpp-preview-image">
-                <img src="" alt="<?php esc_attr_e('Preview', 'studiou-wc-free-photo-product'); ?>" />
-            </div>
-            <div class="studiou-fpp-preview-info">
-                <span class="studiou-fpp-preview-name"></span>
-                <button type="button" class="button studiou-fpp-remove-file"><?php esc_html_e('Remove', 'studiou-wc-free-photo-product'); ?></button>
-            </div>
-        </div>
-    </div>
-
-    <div class="studiou-fpp-section studiou-fpp-actions">
-        <input type="hidden" id="studiou-fpp-attachment-id" value="" />
-        <input type="hidden" id="studiou-fpp-file-record-id" value="" />
-        <div class="studiou-fpp-messages"></div>
-        <button type="button" class="button alt studiou-fpp-add-to-cart-btn" disabled>
-            <?php esc_html_e('Add to Cart', 'studiou-wc-free-photo-product'); ?>
-        </button>
+        <?php woocommerce_variable_add_to_cart(); ?>
     </div>
 
 </div>

+ 47 - 0
studiou-wc-free-photo-product/views/single-product-upload.php

@@ -0,0 +1,47 @@
+<?php
+if (!defined('WPINC')) {
+    die;
+}
+
+/** @var int $product_id */
+/** @var int $max_file_size */
+?>
+<div class="studiou-fpp-upload-wrap" data-product-id="<?php echo esc_attr($product_id); ?>" data-max-file-size="<?php echo esc_attr($max_file_size); ?>">
+
+    <h3><?php esc_html_e('Upload Your Photo', 'studiou-wc-free-photo-product'); ?></h3>
+    <p class="studiou-fpp-upload-info">
+        <?php echo esc_html(sprintf(
+            __('Maximum file size: %s MB. Accepted formats: JPEG, PNG, TIFF, BMP, PSD, RAW (CR2, NEF, ARW, DNG, ORF, RW2, RAF), WebP.', 'studiou-wc-free-photo-product'),
+            $max_file_size
+        )); ?>
+    </p>
+
+    <div class="studiou-fpp-upload-area" id="studiou-fpp-dropzone">
+        <div class="studiou-fpp-upload-prompt">
+            <span class="studiou-fpp-upload-icon dashicons dashicons-upload"></span>
+            <p><?php esc_html_e('Drag & drop your file here or click to browse', 'studiou-wc-free-photo-product'); ?></p>
+        </div>
+        <input type="file" id="studiou-fpp-file-input" class="studiou-fpp-file-input"
+            accept=".jpg,.jpeg,.png,.tiff,.tif,.bmp,.psd,.cr2,.nef,.arw,.dng,.orf,.rw2,.raf,.webp" />
+    </div>
+
+    <div class="studiou-fpp-progress" style="display:none;">
+        <div class="studiou-fpp-progress-bar">
+            <div class="studiou-fpp-progress-fill" style="width:0%"></div>
+        </div>
+        <span class="studiou-fpp-progress-text">0%</span>
+    </div>
+
+    <div class="studiou-fpp-preview" style="display:none;">
+        <div class="studiou-fpp-preview-image">
+            <img src="" alt="<?php esc_attr_e('Preview', 'studiou-wc-free-photo-product'); ?>" />
+        </div>
+        <div class="studiou-fpp-preview-info">
+            <span class="studiou-fpp-preview-name"></span>
+            <button type="button" class="button studiou-fpp-remove-file"><?php esc_html_e('Remove', 'studiou-wc-free-photo-product'); ?></button>
+        </div>
+    </div>
+
+    <div class="studiou-fpp-messages"></div>
+
+</div>