ソースを参照

Add studiou-wc-free-photo-product plugin v1.0.0

WooCommerce plugin for variable products with custom photo upload.
Features: chunked upload, media categories, variant table shortcode,
cart/order integration, admin summary with ZIP download and status
management, HPOS support, EN/CZ localization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dalibor Votruba 3 ヶ月 前
コミット
e979f2685e

+ 62 - 0
studiou-wc-free-photo-product/CLAUDE.md

@@ -0,0 +1,62 @@
+# CLAUDE.md - Studiou WC Free Photo Product
+
+## Project Overview
+
+WordPress/WooCommerce plugin called **studiou-wc-free-photo-product** (QDR - Studiou WC Free Photo Product) v1.0.0.
+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.
+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).
+5. HPOS support and localization (English, Czech).
+
+## Tech Stack
+
+- PHP 8.2+, WordPress 6.9.4+, WooCommerce 10.0+
+- jQuery for frontend/admin JS
+- WordPress AJAX (admin-ajax.php) for all async operations
+- Custom DB table via dbDelta for file tracking
+- Custom taxonomy `studiou_media_category` on `attachment` post type
+
+## Architecture
+
+- Main class: `Studiou_WC_Free_Photo_Product` in root plugin file
+- Constants prefix: `STUDIOU_WCFPP_`
+- CSS class prefix: `studiou-fpp-`
+- JS namespace: `studiouWcfppFront` (frontend), `studiouWcfpp` (admin)
+- Nonces: `studiou-wcfpp-front-nonce` (frontend), `studiou-wcfpp-nonce` (admin)
+- Text domain: `studiou-wc-free-photo-product`
+
+## Key Classes
+
+- `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_Admin` - Summary page, status management, ZIP download
+
+## Coding Conventions
+
+- Follow patterns from sibling plugin `studiou-wc-product-cat-manage`
+- All PHP files start with `if (!defined('WPINC')) { die; }`
+- AJAX handlers: verify nonce, check capabilities, try/catch with WP_DEBUG logging
+- Output buffer cleanup in AJAX handlers
+- Use `wp_send_json_success` / `wp_send_json_error` for all AJAX responses
+- All user-facing strings wrapped in `__()` or `esc_html_e()` with text domain
+- Sanitize all inputs: `absint()`, `sanitize_text_field()`, `sanitize_file_name()`
+- Escape all outputs: `esc_html()`, `esc_attr()`, `esc_url()`, `wp_kses_post()`
+
+## Build / Deploy Notes
+
+- 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`
+- Temp chunks stored in `wp-content/uploads/studiou-fpp-chunks/` (cleaned on deactivation)
+- Assembled files stored in `wp-content/uploads/YYYY/MM/free-photo/`
+
+## Version Bumping
+
+When changing JS or CSS files, always bump the plugin version in the main PHP file header AND the `STUDIOU_WCFPP_VERSION` constant to bust browser cache.

+ 155 - 0
studiou-wc-free-photo-product/assets/css/admin.css

@@ -0,0 +1,155 @@
+/* Product edit - Free Photo tab */
+#woocommerce-product-data ul.wc-tabs li.studiou_fpp_options a::before {
+    content: '\f128';
+    font-family: dashicons;
+}
+
+.studiou-fpp-add-media-cat-btn {
+    margin-left: 5px !important;
+    min-width: 30px;
+    text-align: center;
+}
+
+.studiou-fpp-new-media-cat-form {
+    padding: 0 12px;
+}
+
+.studiou-fpp-new-media-cat-form .form-field {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+.studiou-fpp-new-media-cat-form input[type="text"] {
+    flex: 1;
+}
+
+#studiou-fpp-shortcode-display {
+    background: #f0f0f1;
+    padding: 6px 12px;
+    border-radius: 3px;
+    user-select: all;
+}
+
+/* Admin summary page */
+.studiou-fpp-admin-wrap {
+    max-width: 1400px;
+}
+
+.studiou-fpp-admin-notice-area {
+    margin-bottom: 10px;
+}
+
+.studiou-fpp-filters {
+    margin: 15px 0;
+    padding: 12px 15px;
+    background: #fff;
+    border: 1px solid #ccd0d4;
+    border-radius: 4px;
+}
+
+.studiou-fpp-filters form {
+    display: flex;
+    gap: 8px;
+    align-items: center;
+    flex-wrap: wrap;
+}
+
+.studiou-fpp-filters select,
+.studiou-fpp-filters input[type="search"] {
+    height: 32px;
+}
+
+.studiou-fpp-filters input[type="search"] {
+    min-width: 200px;
+}
+
+.studiou-fpp-bulk-actions {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin: 10px 0;
+    flex-wrap: wrap;
+}
+
+.studiou-fpp-bulk-actions select {
+    height: 32px;
+}
+
+.studiou-fpp-total-count {
+    margin-left: auto;
+    color: #666;
+    font-style: italic;
+}
+
+#studiou-fpp-bulk-spinner {
+    float: none;
+    visibility: hidden;
+}
+
+/* Files table */
+.studiou-fpp-files-table .column-thumbnail {
+    width: 60px;
+}
+
+.studiou-fpp-files-table .column-status {
+    width: 130px;
+}
+
+.studiou-fpp-files-table .column-date {
+    width: 150px;
+}
+
+.studiou-fpp-files-table .column-actions {
+    width: 90px;
+}
+
+.studiou-fpp-files-table .column-order {
+    width: 80px;
+}
+
+.studiou-fpp-thumb {
+    width: 50px;
+    height: 50px;
+    object-fit: cover;
+    border-radius: 3px;
+    display: block;
+}
+
+.studiou-fpp-status-select {
+    width: 100%;
+    height: 28px;
+    font-size: 12px;
+}
+
+.studiou-fpp-no-order {
+    color: #999;
+}
+
+.studiou-fpp-files-table .column-actions {
+    white-space: nowrap;
+}
+
+.studiou-fpp-files-table .column-actions .button {
+    padding: 0 6px;
+    min-height: 28px;
+}
+
+.studiou-fpp-files-table .column-actions .dashicons {
+    font-size: 16px;
+    width: 16px;
+    height: 16px;
+}
+
+/* Status colors */
+.studiou-fpp-status-ready {
+    color: #d63638;
+}
+
+.studiou-fpp-status-downloaded {
+    color: #dba617;
+}
+
+.studiou-fpp-status-solved {
+    color: #00a32a;
+}

+ 283 - 0
studiou-wc-free-photo-product/assets/css/frontend.css

@@ -0,0 +1,283 @@
+.studiou-fpp-product {
+    max-width: 800px;
+    margin: 20px 0;
+    font-family: inherit;
+}
+
+.studiou-fpp-product-header {
+    display: flex;
+    gap: 20px;
+    margin-bottom: 30px;
+    align-items: flex-start;
+}
+
+.studiou-fpp-product-image {
+    flex: 0 0 200px;
+}
+
+.studiou-fpp-product-image img {
+    max-width: 100%;
+    height: auto;
+    border-radius: 4px;
+}
+
+.studiou-fpp-product-info {
+    flex: 1;
+}
+
+.studiou-fpp-product-title {
+    margin: 0 0 10px;
+    font-size: 1.6em;
+}
+
+.studiou-fpp-product-description {
+    color: #555;
+    line-height: 1.6;
+}
+
+.studiou-fpp-section {
+    margin-bottom: 25px;
+}
+
+.studiou-fpp-section h3 {
+    margin: 0 0 12px;
+    font-size: 1.2em;
+    border-bottom: 2px solid #eee;
+    padding-bottom: 8px;
+}
+
+/* Variants table */
+.studiou-fpp-variants-table-wrap {
+    overflow-x: auto;
+}
+
+.studiou-fpp-variants-table {
+    width: 100%;
+    border-collapse: collapse;
+    border: 1px solid #ddd;
+}
+
+.studiou-fpp-variants-table th,
+.studiou-fpp-variants-table td {
+    padding: 10px 14px;
+    text-align: left;
+    border-bottom: 1px solid #eee;
+}
+
+.studiou-fpp-variants-table thead th {
+    background: #f7f7f7;
+    font-weight: 600;
+    border-bottom: 2px solid #ddd;
+}
+
+.studiou-fpp-variants-table .studiou-fpp-col-select {
+    width: 40px;
+    text-align: center;
+}
+
+.studiou-fpp-variants-table .studiou-fpp-col-price {
+    white-space: nowrap;
+    font-weight: 600;
+}
+
+.studiou-fpp-variant-row {
+    cursor: pointer;
+    transition: background 0.15s;
+}
+
+.studiou-fpp-variant-row:hover {
+    background: #f5f9ff;
+}
+
+.studiou-fpp-variant-row.studiou-fpp-variant-selected {
+    background: #e8f4fd;
+}
+
+.studiou-fpp-variant-row input[type="radio"] {
+    margin: 0;
+    cursor: pointer;
+}
+
+/* Upload area */
+.studiou-fpp-upload-info {
+    color: #777;
+    font-size: 0.9em;
+    margin-bottom: 12px;
+}
+
+.studiou-fpp-upload-area {
+    border: 2px dashed #ccc;
+    border-radius: 8px;
+    padding: 40px 20px;
+    text-align: center;
+    cursor: pointer;
+    transition: border-color 0.2s, background 0.2s;
+    position: relative;
+}
+
+.studiou-fpp-upload-area:hover,
+.studiou-fpp-upload-area.studiou-fpp-dragover {
+    border-color: #2271b1;
+    background: #f0f7ff;
+}
+
+.studiou-fpp-upload-icon {
+    font-size: 48px;
+    width: 48px;
+    height: 48px;
+    color: #999;
+    display: block;
+    margin: 0 auto 10px;
+}
+
+.studiou-fpp-upload-area:hover .studiou-fpp-upload-icon {
+    color: #2271b1;
+}
+
+.studiou-fpp-upload-prompt p {
+    margin: 0;
+    color: #777;
+}
+
+.studiou-fpp-file-input {
+    display: none;
+}
+
+/* Progress bar */
+.studiou-fpp-progress {
+    margin-top: 15px;
+    display: flex;
+    align-items: center;
+    gap: 12px;
+}
+
+.studiou-fpp-progress-bar {
+    flex: 1;
+    height: 20px;
+    background: #eee;
+    border-radius: 10px;
+    overflow: hidden;
+}
+
+.studiou-fpp-progress-fill {
+    height: 100%;
+    background: #2271b1;
+    border-radius: 10px;
+    transition: width 0.3s;
+}
+
+.studiou-fpp-progress-text {
+    font-weight: 600;
+    min-width: 40px;
+    text-align: right;
+}
+
+/* Preview */
+.studiou-fpp-preview {
+    display: flex;
+    align-items: center;
+    gap: 15px;
+    padding: 15px;
+    background: #f9f9f9;
+    border: 1px solid #ddd;
+    border-radius: 6px;
+    margin-top: 15px;
+}
+
+.studiou-fpp-preview-image {
+    flex: 0 0 80px;
+}
+
+.studiou-fpp-preview-image img {
+    max-width: 80px;
+    max-height: 80px;
+    border-radius: 4px;
+    display: block;
+}
+
+.studiou-fpp-preview-info {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 10px;
+}
+
+.studiou-fpp-preview-name {
+    font-weight: 500;
+    word-break: break-all;
+}
+
+.studiou-fpp-remove-file {
+    flex-shrink: 0;
+    color: #b32d2e !important;
+    border-color: #b32d2e !important;
+}
+
+.studiou-fpp-remove-file:hover {
+    background: #b32d2e !important;
+    color: #fff !important;
+}
+
+/* Actions / Add to cart */
+.studiou-fpp-actions {
+    padding-top: 15px;
+    border-top: 2px solid #eee;
+}
+
+.studiou-fpp-add-to-cart-btn {
+    padding: 12px 30px !important;
+    font-size: 1.1em !important;
+    font-weight: 600;
+}
+
+.studiou-fpp-add-to-cart-btn:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+}
+
+/* Messages */
+.studiou-fpp-msg {
+    padding: 10px 15px;
+    border-radius: 4px;
+    margin-bottom: 12px;
+    font-size: 0.95em;
+}
+
+.studiou-fpp-msg-error {
+    background: #fef1f1;
+    color: #b32d2e;
+    border: 1px solid #f5c6c6;
+}
+
+.studiou-fpp-msg-success {
+    background: #ecf7ed;
+    color: #1e7e34;
+    border: 1px solid #c3e6c3;
+}
+
+.studiou-fpp-error {
+    color: #b32d2e;
+    font-style: italic;
+}
+
+/* Responsive */
+@media (max-width: 600px) {
+    .studiou-fpp-product-header {
+        flex-direction: column;
+    }
+
+    .studiou-fpp-product-image {
+        flex: none;
+        text-align: center;
+    }
+
+    .studiou-fpp-preview {
+        flex-direction: column;
+        text-align: center;
+    }
+
+    .studiou-fpp-preview-info {
+        flex-direction: column;
+    }
+}

+ 295 - 0
studiou-wc-free-photo-product/assets/js/admin.js

@@ -0,0 +1,295 @@
+(function ($) {
+    'use strict';
+
+    $(document).ready(function () {
+
+        // ========================================
+        // Product edit page: Free Photo tab
+        // ========================================
+        if ($('#studiou_fpp_product_data').length) {
+            initProductTab();
+        }
+
+        // ========================================
+        // Admin summary page
+        // ========================================
+        if ($('.studiou-fpp-admin-wrap').length) {
+            initSummaryPage();
+        }
+
+        function initProductTab() {
+            // Show/hide new media category form
+            $('.studiou-fpp-add-media-cat-btn').on('click', function () {
+                $('.studiou-fpp-new-media-cat-form').toggle();
+            });
+
+            $('.studiou-fpp-cancel-media-cat-btn').on('click', function () {
+                $('.studiou-fpp-new-media-cat-form').hide();
+                $('#studiou_fpp_new_cat_name').val('');
+            });
+
+            // Save new media category
+            $('.studiou-fpp-save-media-cat-btn').on('click', function () {
+                var name = $('#studiou_fpp_new_cat_name').val().trim();
+                if (!name) {
+                    return;
+                }
+
+                var $btn = $(this);
+                $btn.prop('disabled', true);
+
+                $.ajax({
+                    url: studiouWcfpp.ajaxUrl,
+                    type: 'POST',
+                    data: {
+                        action: 'studiou_wcfpp_add_media_category',
+                        nonce: studiouWcfpp.nonce,
+                        name: name
+                    },
+                    success: function (response) {
+                        if (response.success) {
+                            var $select = $('#_studiou_fpp_media_category');
+                            $select.append(
+                                $('<option>', {
+                                    value: response.data.term_id,
+                                    text: response.data.name,
+                                    selected: true
+                                })
+                            );
+                            $('.studiou-fpp-new-media-cat-form').hide();
+                            $('#studiou_fpp_new_cat_name').val('');
+                        } else {
+                            alert(response.data ? response.data.message : studiouWcfpp.i18n.error);
+                        }
+                    },
+                    error: function () {
+                        alert(studiouWcfpp.i18n.error);
+                    },
+                    complete: function () {
+                        $btn.prop('disabled', false);
+                    }
+                });
+            });
+        }
+
+        function initSummaryPage() {
+            var $noticeArea = $('.studiou-fpp-admin-notice-area');
+
+            // Select all
+            $('#studiou-fpp-select-all').on('change', function () {
+                $('.studiou-fpp-file-cb').prop('checked', $(this).is(':checked'));
+            });
+
+            // Individual status change
+            $('.studiou-fpp-status-select').on('change', function () {
+                var fileId = $(this).data('file-id');
+                var status = $(this).val();
+                var $el = $(this);
+
+                $.ajax({
+                    url: studiouWcfpp.ajaxUrl,
+                    type: 'POST',
+                    data: {
+                        action: 'studiou_wcfpp_update_status',
+                        nonce: studiouWcfpp.nonce,
+                        file_id: fileId,
+                        status: status
+                    },
+                    success: function (response) {
+                        if (response.success) {
+                            showAdminNotice(studiouWcfpp.i18n.statusUpdated, 'success');
+                        } else {
+                            showAdminNotice(response.data ? response.data.message : studiouWcfpp.i18n.error, 'error');
+                        }
+                    },
+                    error: function () {
+                        showAdminNotice(studiouWcfpp.i18n.error, 'error');
+                    }
+                });
+            });
+
+            // Delete single
+            $(document).on('click', '.studiou-fpp-delete-single', function () {
+                var fileId = $(this).data('file-id');
+                if (!confirm(studiouWcfpp.i18n.confirmDeleteSingle)) {
+                    return;
+                }
+
+                $.ajax({
+                    url: studiouWcfpp.ajaxUrl,
+                    type: 'POST',
+                    data: {
+                        action: 'studiou_wcfpp_delete_files',
+                        nonce: studiouWcfpp.nonce,
+                        file_ids: [fileId]
+                    },
+                    success: function (response) {
+                        if (response.success) {
+                            $('tr[data-file-id="' + fileId + '"]').fadeOut(300, function () {
+                                $(this).remove();
+                            });
+                            showAdminNotice(response.data.message, 'success');
+                        } else {
+                            showAdminNotice(response.data ? response.data.message : studiouWcfpp.i18n.error, 'error');
+                        }
+                    },
+                    error: function () {
+                        showAdminNotice(studiouWcfpp.i18n.error, 'error');
+                    }
+                });
+            });
+
+            // Bulk action apply
+            $('#studiou-fpp-bulk-apply').on('click', function () {
+                var action = $('#studiou-fpp-bulk-action').val();
+                if (!action) {
+                    return;
+                }
+
+                var fileIds = getSelectedFileIds();
+                if (fileIds.length === 0) {
+                    showAdminNotice(studiouWcfpp.i18n.noFilesSelected, 'error');
+                    return;
+                }
+
+                if (action === 'download_zip') {
+                    bulkDownloadZip(fileIds);
+                } else if (action === 'delete') {
+                    if (!confirm(studiouWcfpp.i18n.confirmDelete)) {
+                        return;
+                    }
+                    bulkDelete(fileIds);
+                } else if (action.startsWith('status_')) {
+                    var status = action.replace('status_', '');
+                    bulkUpdateStatus(fileIds, status);
+                }
+            });
+
+            function getSelectedFileIds() {
+                var ids = [];
+                $('.studiou-fpp-file-cb:checked').each(function () {
+                    ids.push($(this).val());
+                });
+                return ids;
+            }
+
+            function bulkDownloadZip(fileIds) {
+                showBulkSpinner(true);
+                showAdminNotice(studiouWcfpp.i18n.generatingZip, 'info');
+
+                $.ajax({
+                    url: studiouWcfpp.ajaxUrl,
+                    type: 'POST',
+                    data: {
+                        action: 'studiou_wcfpp_download_zip',
+                        nonce: studiouWcfpp.nonce,
+                        file_ids: fileIds
+                    },
+                    success: function (response) {
+                        if (response.success) {
+                            // Trigger download
+                            window.location.href = response.data.download_url;
+                            showAdminNotice(
+                                response.data.file_count + ' file(s) in archive.',
+                                'success'
+                            );
+                        } else {
+                            showAdminNotice(response.data ? response.data.message : studiouWcfpp.i18n.error, 'error');
+                        }
+                    },
+                    error: function () {
+                        showAdminNotice(studiouWcfpp.i18n.error, 'error');
+                    },
+                    complete: function () {
+                        showBulkSpinner(false);
+                    }
+                });
+            }
+
+            function bulkDelete(fileIds) {
+                showBulkSpinner(true);
+
+                $.ajax({
+                    url: studiouWcfpp.ajaxUrl,
+                    type: 'POST',
+                    data: {
+                        action: 'studiou_wcfpp_delete_files',
+                        nonce: studiouWcfpp.nonce,
+                        file_ids: fileIds
+                    },
+                    success: function (response) {
+                        if (response.success) {
+                            fileIds.forEach(function (id) {
+                                $('tr[data-file-id="' + id + '"]').fadeOut(300, function () {
+                                    $(this).remove();
+                                });
+                            });
+                            showAdminNotice(response.data.message, 'success');
+                        } else {
+                            showAdminNotice(response.data ? response.data.message : studiouWcfpp.i18n.error, 'error');
+                        }
+                    },
+                    error: function () {
+                        showAdminNotice(studiouWcfpp.i18n.error, 'error');
+                    },
+                    complete: function () {
+                        showBulkSpinner(false);
+                    }
+                });
+            }
+
+            function bulkUpdateStatus(fileIds, status) {
+                showBulkSpinner(true);
+
+                $.ajax({
+                    url: studiouWcfpp.ajaxUrl,
+                    type: 'POST',
+                    data: {
+                        action: 'studiou_wcfpp_bulk_status',
+                        nonce: studiouWcfpp.nonce,
+                        file_ids: fileIds,
+                        status: status
+                    },
+                    success: function (response) {
+                        if (response.success) {
+                            // Update dropdowns
+                            fileIds.forEach(function (id) {
+                                $('tr[data-file-id="' + id + '"] .studiou-fpp-status-select').val(status);
+                            });
+                            showAdminNotice(response.data.message, 'success');
+                        } else {
+                            showAdminNotice(response.data ? response.data.message : studiouWcfpp.i18n.error, 'error');
+                        }
+                    },
+                    error: function () {
+                        showAdminNotice(studiouWcfpp.i18n.error, 'error');
+                    },
+                    complete: function () {
+                        showBulkSpinner(false);
+                    }
+                });
+            }
+
+            function showBulkSpinner(show) {
+                $('#studiou-fpp-bulk-spinner').css('visibility', show ? 'visible' : 'hidden');
+            }
+
+            function showAdminNotice(message, type) {
+                var cls = 'notice-' + (type === 'error' ? 'error' : (type === 'info' ? 'info' : 'success'));
+                $noticeArea.html(
+                    '<div class="notice ' + cls + ' is-dismissible"><p>' + escHtml(message) + '</p>' +
+                    '<button type="button" class="notice-dismiss"><span class="screen-reader-text">Dismiss</span></button></div>'
+                );
+                $noticeArea.find('.notice-dismiss').on('click', function () {
+                    $(this).closest('.notice').fadeOut(200, function () { $(this).remove(); });
+                });
+            }
+        }
+
+        function escHtml(str) {
+            var div = document.createElement('div');
+            div.appendChild(document.createTextNode(str));
+            return div.innerHTML;
+        }
+    });
+})(jQuery);

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

@@ -0,0 +1,287 @@
+(function ($) {
+    'use strict';
+
+    $(document).ready(function () {
+        var $container = $('.studiou-fpp-product');
+        if (!$container.length) {
+            return;
+        }
+
+        var productId = $container.data('product-id');
+        var maxFileSize = parseInt($container.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');
+        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();
+        });
+
+        // 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');
+        });
+
+        // Drag and drop
+        $dropzone.on('dragover dragenter', function (e) {
+            e.preventDefault();
+            e.stopPropagation();
+            $(this).addClass('studiou-fpp-dragover');
+        });
+
+        $dropzone.on('dragleave drop', function (e) {
+            e.preventDefault();
+            e.stopPropagation();
+            $(this).removeClass('studiou-fpp-dragover');
+        });
+
+        $dropzone.on('drop', function (e) {
+            var files = e.originalEvent.dataTransfer.files;
+            if (files.length > 0) {
+                handleFile(files[0]);
+            }
+        });
+
+        $dropzone.on('click', function (e) {
+            if (!isUploading) {
+                $fileInput.trigger('click');
+            }
+        });
+
+        $fileInput.on('change', function () {
+            if (this.files.length > 0) {
+                handleFile(this.files[0]);
+            }
+        });
+
+        // Remove file
+        $removeBtn.on('click', function () {
+            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');
+                return;
+            }
+
+            clearMessages();
+            startChunkedUpload(file);
+        }
+
+        function startChunkedUpload(file) {
+            isUploading = true;
+            var totalChunks = Math.ceil(file.size / chunkSize);
+            var uploadId = 'upload_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
+            var currentChunk = 0;
+
+            $dropzone.hide();
+            $progress.show();
+            $addToCartBtn.prop('disabled', true);
+
+            function uploadNextChunk() {
+                var start = currentChunk * chunkSize;
+                var end = Math.min(start + chunkSize, file.size);
+                var blob = file.slice(start, end);
+
+                var formData = new FormData();
+                formData.append('action', 'studiou_wcfpp_upload_chunk');
+                formData.append('nonce', studiouWcfppFront.nonce);
+                formData.append('product_id', productId);
+                formData.append('upload_id', uploadId);
+                formData.append('chunk_index', currentChunk);
+                formData.append('total_chunks', totalChunks);
+                formData.append('file_name', file.name);
+                formData.append('file_size', file.size);
+                formData.append('chunk', blob, 'chunk');
+
+                $.ajax({
+                    url: studiouWcfppFront.ajaxUrl,
+                    type: 'POST',
+                    data: formData,
+                    processData: false,
+                    contentType: false,
+                    success: function (response) {
+                        if (response.success) {
+                            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);
+
+                                showPreview(response.data.thumbnail_url, response.data.file_name);
+                                $progress.hide();
+                                isUploading = false;
+                                updateAddToCartState();
+                            } else {
+                                // Upload next chunk
+                                uploadNextChunk();
+                            }
+                        } else {
+                            uploadFailed(response.data ? response.data.message : studiouWcfppFront.i18n.uploadError);
+                        }
+                    },
+                    error: function () {
+                        uploadFailed(studiouWcfppFront.i18n.uploadError);
+                    }
+                });
+            }
+
+            uploadNextChunk();
+        }
+
+        function uploadFailed(message) {
+            isUploading = false;
+            $progress.hide();
+            $dropzone.show();
+            showMessage(message, 'error');
+            $fileInput.val('');
+        }
+
+        function showPreview(thumbnailUrl, fileName) {
+            if (thumbnailUrl) {
+                $previewImg.attr('src', thumbnailUrl).show();
+            } else {
+                $previewImg.hide();
+            }
+            $previewName.text(fileName);
+            $preview.show();
+        }
+
+        function removeUploadedFile() {
+            if (!uploadedFileRecordId) {
+                return;
+            }
+
+            $.ajax({
+                url: studiouWcfppFront.ajaxUrl,
+                type: 'POST',
+                data: {
+                    action: 'studiou_wcfpp_remove_upload',
+                    nonce: studiouWcfppFront.nonce,
+                    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();
+                }
+            });
+        }
+
+        function addToCart() {
+            if (!selectedVariation) {
+                showMessage(studiouWcfppFront.i18n.selectVariant, 'error');
+                return;
+            }
+            if (!uploadedAttachmentId) {
+                showMessage(studiouWcfppFront.i18n.uploadFile, 'error');
+                return;
+            }
+
+            $addToCartBtn.prop('disabled', true);
+            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) {
+            var cls = type === 'error' ? 'studiou-fpp-msg-error' : 'studiou-fpp-msg-success';
+            $messages.html('<div class="studiou-fpp-msg ' + cls + '">' + escHtml(text) + '</div>');
+        }
+
+        function clearMessages() {
+            $messages.empty();
+        }
+
+        function escHtml(str) {
+            var div = document.createElement('div');
+            div.appendChild(document.createTextNode(str));
+            return div.innerHTML;
+        }
+    });
+})(jQuery);

+ 279 - 0
studiou-wc-free-photo-product/includes/class-studiou-wc-fpp-admin.php

@@ -0,0 +1,279 @@
+<?php
+if (!defined('WPINC')) {
+    die;
+}
+
+class Studiou_WC_FPP_Admin {
+
+    /** @var Studiou_WC_FPP_DB */
+    private $db;
+
+    public function __construct($db) {
+        $this->db = $db;
+
+        // AJAX handlers
+        add_action('wp_ajax_studiou_wcfpp_update_status', array($this, 'ajax_update_status'));
+        add_action('wp_ajax_studiou_wcfpp_delete_files', array($this, 'ajax_delete_files'));
+        add_action('wp_ajax_studiou_wcfpp_download_zip', array($this, 'ajax_download_zip'));
+        add_action('wp_ajax_studiou_wcfpp_bulk_status', array($this, 'ajax_bulk_status'));
+
+        // Handle direct ZIP download
+        add_action('admin_init', array($this, 'handle_zip_download'));
+    }
+
+    public function render_summary_page() {
+        // Process filters
+        $current_status = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : '';
+        $current_product = isset($_GET['product_id']) ? absint($_GET['product_id']) : 0;
+        $current_search = isset($_GET['s']) ? sanitize_text_field($_GET['s']) : '';
+        $current_page = isset($_GET['paged']) ? max(1, absint($_GET['paged'])) : 1;
+        $per_page = 30;
+
+        $args = array(
+            'status'  => $current_status,
+            'product_id' => $current_product,
+            'search'  => $current_search,
+            'limit'   => $per_page,
+            'offset'  => ($current_page - 1) * $per_page,
+            'orderby' => isset($_GET['orderby']) ? sanitize_text_field($_GET['orderby']) : 'created_at',
+            'order'   => isset($_GET['order']) ? sanitize_text_field($_GET['order']) : 'DESC',
+        );
+
+        $records = $this->db->get_file_records($args);
+        $total = $this->db->count_file_records($args);
+        $total_pages = ceil($total / $per_page);
+
+        // Get products that have FPP enabled for filter dropdown
+        $fpp_products = $this->get_fpp_products();
+
+        include STUDIOU_WCFPP_PLUGIN_DIR . 'views/admin-summary.php';
+    }
+
+    private function get_fpp_products() {
+        $args = array(
+            'post_type'      => 'product',
+            'post_status'    => 'publish',
+            'posts_per_page' => -1,
+            'meta_query'     => array(
+                array(
+                    'key'   => '_studiou_fpp_enabled',
+                    'value' => 'yes',
+                ),
+            ),
+            'fields' => 'ids',
+        );
+        $product_ids = get_posts($args);
+        $products = array();
+        foreach ($product_ids as $pid) {
+            $products[$pid] = get_the_title($pid);
+        }
+        return $products;
+    }
+
+    public function ajax_update_status() {
+        check_ajax_referer('studiou-wcfpp-nonce', 'nonce');
+
+        if (!current_user_can('manage_woocommerce')) {
+            wp_send_json_error(array('message' => __('Insufficient permissions.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        $file_id = isset($_POST['file_id']) ? absint($_POST['file_id']) : 0;
+        $status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : '';
+
+        if (!$file_id || !$status) {
+            wp_send_json_error(array('message' => __('Invalid data.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        $result = $this->db->update_file_status($file_id, $status);
+        if ($result === false) {
+            wp_send_json_error(array('message' => __('Failed to update status.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        wp_send_json_success(array('message' => __('Status updated.', 'studiou-wc-free-photo-product')));
+    }
+
+    public function ajax_bulk_status() {
+        check_ajax_referer('studiou-wcfpp-nonce', 'nonce');
+
+        if (!current_user_can('manage_woocommerce')) {
+            wp_send_json_error(array('message' => __('Insufficient permissions.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        $file_ids = isset($_POST['file_ids']) ? array_map('absint', (array) $_POST['file_ids']) : array();
+        $status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : '';
+
+        if (empty($file_ids) || !$status) {
+            wp_send_json_error(array('message' => __('Invalid data.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        $updated = 0;
+        foreach ($file_ids as $fid) {
+            if ($this->db->update_file_status($fid, $status)) {
+                $updated++;
+            }
+        }
+
+        wp_send_json_success(array(
+            'message' => sprintf(__('%d file(s) updated.', 'studiou-wc-free-photo-product'), $updated),
+        ));
+    }
+
+    public function ajax_delete_files() {
+        check_ajax_referer('studiou-wcfpp-nonce', 'nonce');
+
+        if (!current_user_can('manage_woocommerce')) {
+            wp_send_json_error(array('message' => __('Insufficient permissions.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        $file_ids = isset($_POST['file_ids']) ? array_map('absint', (array) $_POST['file_ids']) : array();
+        if (empty($file_ids)) {
+            wp_send_json_error(array('message' => __('No files selected.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        $deleted = 0;
+        foreach ($file_ids as $fid) {
+            if ($this->db->delete_file_record($fid)) {
+                $deleted++;
+            }
+        }
+
+        wp_send_json_success(array(
+            'message' => sprintf(__('%d file(s) deleted.', 'studiou-wc-free-photo-product'), $deleted),
+        ));
+    }
+
+    public function ajax_download_zip() {
+        check_ajax_referer('studiou-wcfpp-nonce', 'nonce');
+
+        if (!current_user_can('manage_woocommerce')) {
+            wp_send_json_error(array('message' => __('Insufficient permissions.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        $file_ids = isset($_POST['file_ids']) ? array_map('absint', (array) $_POST['file_ids']) : array();
+        if (empty($file_ids)) {
+            wp_send_json_error(array('message' => __('No files selected.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        $records = $this->db->get_file_records_by_ids($file_ids);
+        if (empty($records)) {
+            wp_send_json_error(array('message' => __('No files found.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        // Generate ZIP
+        $upload_dir = wp_upload_dir();
+        $zip_name = 'fpp-files-' . date('Y-m-d-His') . '.zip';
+        $zip_path = $upload_dir['basedir'] . '/studiou-fpp-chunks/' . $zip_name;
+
+        if (!class_exists('ZipArchive')) {
+            wp_send_json_error(array('message' => __('ZIP extension not available on server.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        $zip = new ZipArchive();
+        if ($zip->open($zip_path, ZipArchive::CREATE) !== true) {
+            wp_send_json_error(array('message' => __('Could not create ZIP file.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        $added = 0;
+        foreach ($records as $record) {
+            $file_path = get_attached_file($record->attachment_id);
+            if ($file_path && file_exists($file_path)) {
+                // Use order number prefix if available
+                $prefix = $record->order_id ? 'order-' . $record->order_id . '_' : '';
+                $zip->addFile($file_path, $prefix . $record->file_name);
+                $added++;
+            }
+        }
+        $zip->close();
+
+        if ($added === 0) {
+            if (file_exists($zip_path)) {
+                unlink($zip_path);
+            }
+            wp_send_json_error(array('message' => __('No files could be added to archive.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        // Generate a nonce-protected download URL
+        $download_url = add_query_arg(array(
+            'studiou_fpp_download' => 1,
+            'file' => $zip_name,
+            '_wpnonce' => wp_create_nonce('studiou-fpp-zip-download'),
+        ), admin_url('admin.php'));
+
+        wp_send_json_success(array(
+            'download_url' => $download_url,
+            'file_count'   => $added,
+        ));
+    }
+
+    public function handle_zip_download() {
+        if (!isset($_GET['studiou_fpp_download']) || !isset($_GET['file'])) {
+            return;
+        }
+
+        if (!wp_verify_nonce($_GET['_wpnonce'], 'studiou-fpp-zip-download')) {
+            wp_die(__('Security check failed.', 'studiou-wc-free-photo-product'));
+        }
+
+        if (!current_user_can('manage_woocommerce')) {
+            wp_die(__('Insufficient permissions.', 'studiou-wc-free-photo-product'));
+        }
+
+        $file_name = sanitize_file_name($_GET['file']);
+        $upload_dir = wp_upload_dir();
+        $file_path = $upload_dir['basedir'] . '/studiou-fpp-chunks/' . $file_name;
+
+        if (!file_exists($file_path) || pathinfo($file_name, PATHINFO_EXTENSION) !== 'zip') {
+            wp_die(__('File not found.', 'studiou-wc-free-photo-product'));
+        }
+
+        header('Content-Type: application/zip');
+        header('Content-Disposition: attachment; filename="' . $file_name . '"');
+        header('Content-Length: ' . filesize($file_path));
+        header('Pragma: no-cache');
+
+        readfile($file_path);
+
+        // Clean up the ZIP file after download
+        unlink($file_path);
+        exit;
+    }
+
+    public static function get_status_label($status) {
+        $labels = array(
+            'ready'      => __('Ready', 'studiou-wc-free-photo-product'),
+            'downloaded' => __('Downloaded', 'studiou-wc-free-photo-product'),
+            'solved'     => __('Solved', 'studiou-wc-free-photo-product'),
+        );
+        return isset($labels[$status]) ? $labels[$status] : $status;
+    }
+
+    public static function get_status_class($status) {
+        $classes = array(
+            'ready'      => 'studiou-fpp-status-ready',
+            'downloaded' => 'studiou-fpp-status-downloaded',
+            'solved'     => 'studiou-fpp-status-solved',
+        );
+        return isset($classes[$status]) ? $classes[$status] : '';
+    }
+
+    public static function get_order_edit_url($order_id) {
+        if (class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
+            && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled()) {
+            return admin_url('admin.php?page=wc-orders&action=edit&id=' . $order_id);
+        }
+        return admin_url('post.php?post=' . $order_id . '&action=edit');
+    }
+}

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

@@ -0,0 +1,140 @@
+<?php
+if (!defined('WPINC')) {
+    die;
+}
+
+class Studiou_WC_FPP_Cart {
+
+    /** @var Studiou_WC_FPP_DB */
+    private $db;
+
+    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'));
+
+        // Display uploaded file info in cart
+        add_filter('woocommerce_get_item_data', array($this, 'display_cart_item_data'), 10, 2);
+
+        // 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);
+
+        // 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
+        if (!Studiou_WC_FPP_Product::is_fpp_product($product_id)) {
+            wp_send_json_error(array('message' => __('Invalid product.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        // 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;
+        }
+
+        // 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;
+        }
+
+        // Get variation attributes for cart
+        $variation_data = array();
+        $attributes = $variation->get_variation_attributes();
+        foreach ($attributes as $key => $value) {
+            $variation_data[$key] = $value;
+        }
+
+        // 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,
+        );
+
+        // Update file record with variation
+        $this->db->update_file_record($file_record_id, array(
+            'variation_id' => $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')));
+        }
+    }
+
+    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;
+    }
+
+    public function display_cart_item_data($item_data, $cart_item) {
+        if (isset($cart_item['studiou_fpp_file_name'])) {
+            $item_data[] = array(
+                'name'    => __('Uploaded Photo', 'studiou-wc-free-photo-product'),
+                'value'   => esc_html($cart_item['studiou_fpp_file_name']),
+                'display' => '',
+            );
+        }
+        return $item_data;
+    }
+
+    public function save_order_item_meta($item, $cart_item_key, $values, $order) {
+        if (isset($values['studiou_fpp_attachment_id'])) {
+            $item->add_meta_data('_studiou_fpp_attachment_id', $values['studiou_fpp_attachment_id']);
+        }
+        if (isset($values['studiou_fpp_file_record_id'])) {
+            $item->add_meta_data('_studiou_fpp_file_record_id', $values['studiou_fpp_file_record_id']);
+        }
+        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']);
+        }
+    }
+
+    public function link_files_to_order($order_id, $posted_data, $order) {
+        foreach ($order->get_items() as $item_id => $item) {
+            $file_record_id = $item->get_meta('_studiou_fpp_file_record_id');
+            if ($file_record_id) {
+                $this->db->link_file_to_order((int) $file_record_id, $order_id, $item_id);
+            }
+        }
+    }
+}

+ 283 - 0
studiou-wc-free-photo-product/includes/class-studiou-wc-fpp-db.php

@@ -0,0 +1,283 @@
+<?php
+if (!defined('WPINC')) {
+    die;
+}
+
+class Studiou_WC_FPP_DB {
+
+    const TABLE_NAME = 'studiou_fpp_files';
+
+    public function __construct() {
+        $this->register_media_taxonomy();
+    }
+
+    private function register_media_taxonomy() {
+        add_action('init', array($this, 'register_media_category_taxonomy'));
+    }
+
+    public function register_media_category_taxonomy() {
+        $labels = array(
+            'name'              => __('Media Categories', 'studiou-wc-free-photo-product'),
+            'singular_name'     => __('Media Category', 'studiou-wc-free-photo-product'),
+            'search_items'      => __('Search Media Categories', 'studiou-wc-free-photo-product'),
+            'all_items'         => __('All Media Categories', 'studiou-wc-free-photo-product'),
+            'parent_item'       => __('Parent Media Category', 'studiou-wc-free-photo-product'),
+            'parent_item_colon' => __('Parent Media Category:', 'studiou-wc-free-photo-product'),
+            'edit_item'         => __('Edit Media Category', 'studiou-wc-free-photo-product'),
+            'update_item'       => __('Update Media Category', 'studiou-wc-free-photo-product'),
+            'add_new_item'      => __('Add New Media Category', 'studiou-wc-free-photo-product'),
+            'new_item_name'     => __('New Media Category Name', 'studiou-wc-free-photo-product'),
+            'menu_name'         => __('Media Categories', 'studiou-wc-free-photo-product'),
+        );
+
+        register_taxonomy('studiou_media_category', 'attachment', array(
+            'hierarchical'      => true,
+            'labels'            => $labels,
+            'show_ui'           => true,
+            'show_admin_column' => true,
+            'query_var'         => true,
+            'rewrite'           => array('slug' => 'media-category'),
+            'show_in_rest'      => true,
+        ));
+    }
+
+    public function get_table_name() {
+        global $wpdb;
+        return $wpdb->prefix . self::TABLE_NAME;
+    }
+
+    public function create_tables() {
+        global $wpdb;
+
+        $table_name = $this->get_table_name();
+        $charset_collate = $wpdb->get_charset_collate();
+
+        $sql = "CREATE TABLE {$table_name} (
+            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+            product_id bigint(20) unsigned NOT NULL,
+            variation_id bigint(20) unsigned DEFAULT 0,
+            order_id bigint(20) unsigned DEFAULT 0,
+            order_item_id bigint(20) unsigned DEFAULT 0,
+            attachment_id bigint(20) unsigned NOT NULL,
+            customer_id bigint(20) unsigned DEFAULT 0,
+            session_key varchar(64) DEFAULT '',
+            file_name varchar(255) NOT NULL,
+            status varchar(20) NOT NULL DEFAULT 'ready',
+            created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+            updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+            PRIMARY KEY (id),
+            KEY product_id (product_id),
+            KEY order_id (order_id),
+            KEY attachment_id (attachment_id),
+            KEY status (status),
+            KEY customer_id (customer_id)
+        ) {$charset_collate};";
+
+        require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
+        dbDelta($sql);
+
+        update_option('studiou_wcfpp_db_version', STUDIOU_WCFPP_VERSION);
+    }
+
+    public function insert_file_record($data) {
+        global $wpdb;
+
+        $defaults = array(
+            'product_id'    => 0,
+            'variation_id'  => 0,
+            'order_id'      => 0,
+            'order_item_id' => 0,
+            'attachment_id' => 0,
+            'customer_id'   => get_current_user_id(),
+            'session_key'   => '',
+            'file_name'     => '',
+            'status'        => 'ready',
+            'created_at'    => current_time('mysql'),
+            'updated_at'    => current_time('mysql'),
+        );
+
+        $data = wp_parse_args($data, $defaults);
+
+        $result = $wpdb->insert(
+            $this->get_table_name(),
+            $data,
+            array('%d', '%d', '%d', '%d', '%d', '%d', '%s', '%s', '%s', '%s', '%s')
+        );
+
+        if ($result === false) {
+            return false;
+        }
+
+        return $wpdb->insert_id;
+    }
+
+    public function get_file_record($id) {
+        global $wpdb;
+        $table = $this->get_table_name();
+        return $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $id));
+    }
+
+    public function get_file_records($args = array()) {
+        global $wpdb;
+        $table = $this->get_table_name();
+
+        $defaults = array(
+            'product_id' => 0,
+            'order_id'   => 0,
+            'status'     => '',
+            'orderby'    => 'created_at',
+            'order'      => 'DESC',
+            'limit'      => 50,
+            'offset'     => 0,
+            'search'     => '',
+        );
+        $args = wp_parse_args($args, $defaults);
+
+        $where = array('1=1');
+        $values = array();
+
+        if (!empty($args['product_id'])) {
+            $where[] = 'product_id = %d';
+            $values[] = $args['product_id'];
+        }
+        if (!empty($args['order_id'])) {
+            $where[] = 'order_id = %d';
+            $values[] = $args['order_id'];
+        }
+        if (!empty($args['status'])) {
+            $where[] = 'status = %s';
+            $values[] = $args['status'];
+        }
+        if (!empty($args['search'])) {
+            $where[] = 'file_name LIKE %s';
+            $values[] = '%' . $wpdb->esc_like($args['search']) . '%';
+        }
+
+        $where_clause = implode(' AND ', $where);
+
+        $allowed_orderby = array('id', 'product_id', 'order_id', 'file_name', 'status', 'created_at', 'updated_at');
+        $orderby = in_array($args['orderby'], $allowed_orderby) ? $args['orderby'] : 'created_at';
+        $order = strtoupper($args['order']) === 'ASC' ? 'ASC' : 'DESC';
+
+        $sql = "SELECT * FROM {$table} WHERE {$where_clause} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d";
+        $values[] = (int) $args['limit'];
+        $values[] = (int) $args['offset'];
+
+        if (!empty($values)) {
+            $sql = $wpdb->prepare($sql, $values);
+        }
+
+        return $wpdb->get_results($sql);
+    }
+
+    public function count_file_records($args = array()) {
+        global $wpdb;
+        $table = $this->get_table_name();
+
+        $where = array('1=1');
+        $values = array();
+
+        if (!empty($args['product_id'])) {
+            $where[] = 'product_id = %d';
+            $values[] = $args['product_id'];
+        }
+        if (!empty($args['order_id'])) {
+            $where[] = 'order_id = %d';
+            $values[] = $args['order_id'];
+        }
+        if (!empty($args['status'])) {
+            $where[] = 'status = %s';
+            $values[] = $args['status'];
+        }
+        if (!empty($args['search'])) {
+            $where[] = 'file_name LIKE %s';
+            $values[] = '%' . $wpdb->esc_like($args['search']) . '%';
+        }
+
+        $where_clause = implode(' AND ', $where);
+        $sql = "SELECT COUNT(*) FROM {$table} WHERE {$where_clause}";
+
+        if (!empty($values)) {
+            $sql = $wpdb->prepare($sql, $values);
+        }
+
+        return (int) $wpdb->get_var($sql);
+    }
+
+    public function update_file_record($id, $data) {
+        global $wpdb;
+
+        $data['updated_at'] = current_time('mysql');
+
+        return $wpdb->update(
+            $this->get_table_name(),
+            $data,
+            array('id' => $id),
+            null,
+            array('%d')
+        );
+    }
+
+    public function update_file_status($id, $status) {
+        $allowed = array('ready', 'downloaded', 'solved');
+        if (!in_array($status, $allowed)) {
+            return false;
+        }
+        return $this->update_file_record($id, array('status' => $status));
+    }
+
+    public function delete_file_record($id) {
+        global $wpdb;
+
+        $record = $this->get_file_record($id);
+        if (!$record) {
+            return false;
+        }
+
+        // Delete the attachment from media library
+        if ($record->attachment_id) {
+            wp_delete_attachment($record->attachment_id, true);
+        }
+
+        return $wpdb->delete(
+            $this->get_table_name(),
+            array('id' => $id),
+            array('%d')
+        );
+    }
+
+    public function get_file_records_by_ids($ids) {
+        global $wpdb;
+        $table = $this->get_table_name();
+
+        if (empty($ids)) {
+            return array();
+        }
+
+        $ids = array_map('intval', $ids);
+        $placeholders = implode(',', array_fill(0, count($ids), '%d'));
+        $sql = $wpdb->prepare("SELECT * FROM {$table} WHERE id IN ({$placeholders})", $ids);
+
+        return $wpdb->get_results($sql);
+    }
+
+    public function link_file_to_order($file_record_id, $order_id, $order_item_id) {
+        return $this->update_file_record($file_record_id, array(
+            'order_id'      => $order_id,
+            'order_item_id' => $order_item_id,
+        ));
+    }
+
+    public function get_media_categories() {
+        $terms = get_terms(array(
+            'taxonomy'   => 'studiou_media_category',
+            'hide_empty' => false,
+        ));
+
+        if (is_wp_error($terms)) {
+            return array();
+        }
+
+        return $terms;
+    }
+}

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

@@ -0,0 +1,168 @@
+<?php
+if (!defined('WPINC')) {
+    die;
+}
+
+class Studiou_WC_FPP_Product {
+
+    public function __construct() {
+        // Add product data tab
+        add_filter('woocommerce_product_data_tabs', array($this, 'add_product_data_tab'));
+        add_action('woocommerce_product_data_panels', array($this, 'add_product_data_panel'));
+        add_action('woocommerce_process_product_meta', array($this, 'save_product_meta'));
+
+        // AJAX for adding new media category from product edit page
+        add_action('wp_ajax_studiou_wcfpp_add_media_category', array($this, 'ajax_add_media_category'));
+    }
+
+    public function add_product_data_tab($tabs) {
+        $tabs['studiou_fpp'] = array(
+            'label'    => __('Free Photo', 'studiou-wc-free-photo-product'),
+            'target'   => 'studiou_fpp_product_data',
+            'class'    => array('show_if_variable'),
+            'priority' => 80,
+        );
+        return $tabs;
+    }
+
+    public function add_product_data_panel() {
+        global $post;
+        $product_id = $post->ID;
+
+        $enabled = get_post_meta($product_id, '_studiou_fpp_enabled', true);
+        $media_category = get_post_meta($product_id, '_studiou_fpp_media_category', true);
+        $max_file_size = get_post_meta($product_id, '_studiou_fpp_max_file_size', true);
+
+        if (empty($max_file_size)) {
+            $max_file_size = 50;
+        }
+
+        $categories = get_terms(array(
+            'taxonomy'   => 'studiou_media_category',
+            'hide_empty' => false,
+        ));
+        if (is_wp_error($categories)) {
+            $categories = array();
+        }
+        ?>
+        <div id="studiou_fpp_product_data" class="panel woocommerce_options_panel">
+            <div class="options_group">
+                <?php
+                woocommerce_wp_checkbox(array(
+                    'id'          => '_studiou_fpp_enabled',
+                    'label'       => __('Enable Free Photo', 'studiou-wc-free-photo-product'),
+                    'description' => __('Enable custom photo upload for this product', 'studiou-wc-free-photo-product'),
+                    'value'       => $enabled,
+                ));
+                ?>
+            </div>
+
+            <div class="options_group">
+                <p class="form-field _studiou_fpp_media_category_field">
+                    <label for="_studiou_fpp_media_category"><?php esc_html_e('Media Category', 'studiou-wc-free-photo-product'); ?></label>
+                    <select id="_studiou_fpp_media_category" name="_studiou_fpp_media_category" class="select short">
+                        <option value=""><?php esc_html_e('— Select category —', 'studiou-wc-free-photo-product'); ?></option>
+                        <?php foreach ($categories as $cat) : ?>
+                            <option value="<?php echo esc_attr($cat->term_id); ?>" <?php selected($media_category, $cat->term_id); ?>>
+                                <?php echo esc_html($cat->name); ?>
+                            </option>
+                        <?php endforeach; ?>
+                    </select>
+                    <button type="button" class="button studiou-fpp-add-media-cat-btn" title="<?php esc_attr_e('Add New Category', 'studiou-wc-free-photo-product'); ?>">+</button>
+                    <?php echo wc_help_tip(__('Uploaded files will be assigned to this media category.', 'studiou-wc-free-photo-product')); ?>
+                </p>
+
+                <div class="studiou-fpp-new-media-cat-form" style="display:none;">
+                    <p class="form-field">
+                        <label for="studiou_fpp_new_cat_name"><?php esc_html_e('New Category Name', 'studiou-wc-free-photo-product'); ?></label>
+                        <input type="text" id="studiou_fpp_new_cat_name" class="short" />
+                        <button type="button" class="button studiou-fpp-save-media-cat-btn"><?php esc_html_e('Add', 'studiou-wc-free-photo-product'); ?></button>
+                        <button type="button" class="button studiou-fpp-cancel-media-cat-btn"><?php esc_html_e('Cancel', 'studiou-wc-free-photo-product'); ?></button>
+                    </p>
+                </div>
+            </div>
+
+            <div class="options_group">
+                <?php
+                woocommerce_wp_text_input(array(
+                    'id'                => '_studiou_fpp_max_file_size',
+                    'label'             => __('Max File Size (MB)', 'studiou-wc-free-photo-product'),
+                    'description'       => __('Maximum allowed file size for uploads in megabytes.', 'studiou-wc-free-photo-product'),
+                    'desc_tip'          => true,
+                    'type'              => 'number',
+                    'custom_attributes' => array(
+                        'step' => '1',
+                        'min'  => '1',
+                        'max'  => '500',
+                    ),
+                    'value'             => $max_file_size,
+                ));
+                ?>
+            </div>
+
+            <div class="options_group">
+                <p class="form-field">
+                    <label><?php esc_html_e('Shortcode', 'studiou-wc-free-photo-product'); ?></label>
+                    <code id="studiou-fpp-shortcode-display">[studiou_free_photo_product id="<?php echo esc_attr($product_id); ?>"]</code>
+                    <?php echo wc_help_tip(__('Use this shortcode to display this product with photo upload on any page.', 'studiou-wc-free-photo-product')); ?>
+                </p>
+            </div>
+        </div>
+        <?php
+    }
+
+    public function save_product_meta($product_id) {
+        $enabled = isset($_POST['_studiou_fpp_enabled']) ? 'yes' : 'no';
+        update_post_meta($product_id, '_studiou_fpp_enabled', $enabled);
+
+        if (isset($_POST['_studiou_fpp_media_category'])) {
+            update_post_meta($product_id, '_studiou_fpp_media_category', absint($_POST['_studiou_fpp_media_category']));
+        }
+
+        if (isset($_POST['_studiou_fpp_max_file_size'])) {
+            $max_size = absint($_POST['_studiou_fpp_max_file_size']);
+            if ($max_size < 1) $max_size = 1;
+            if ($max_size > 500) $max_size = 500;
+            update_post_meta($product_id, '_studiou_fpp_max_file_size', $max_size);
+        }
+    }
+
+    public function ajax_add_media_category() {
+        check_ajax_referer('studiou-wcfpp-nonce', 'nonce');
+
+        if (!current_user_can('manage_woocommerce')) {
+            wp_send_json_error(array('message' => __('Insufficient permissions.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        $name = isset($_POST['name']) ? sanitize_text_field($_POST['name']) : '';
+        if (empty($name)) {
+            wp_send_json_error(array('message' => __('Category name is required.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        $term = wp_insert_term($name, 'studiou_media_category');
+        if (is_wp_error($term)) {
+            wp_send_json_error(array('message' => $term->get_error_message()));
+            return;
+        }
+
+        wp_send_json_success(array(
+            'term_id' => $term['term_id'],
+            'name'    => $name,
+        ));
+    }
+
+    public static function is_fpp_product($product_id) {
+        return get_post_meta($product_id, '_studiou_fpp_enabled', true) === 'yes';
+    }
+
+    public static function get_product_max_file_size($product_id) {
+        $size = get_post_meta($product_id, '_studiou_fpp_max_file_size', true);
+        return !empty($size) ? (int) $size : 50;
+    }
+
+    public static function get_product_media_category($product_id) {
+        return (int) get_post_meta($product_id, '_studiou_fpp_media_category', true);
+    }
+}

+ 48 - 0
studiou-wc-free-photo-product/includes/class-studiou-wc-fpp-shortcode.php

@@ -0,0 +1,48 @@
+<?php
+if (!defined('WPINC')) {
+    die;
+}
+
+class Studiou_WC_FPP_Shortcode {
+
+    public function __construct() {
+        add_shortcode('studiou_free_photo_product', array($this, 'render_shortcode'));
+    }
+
+    public function render_shortcode($atts) {
+        $atts = shortcode_atts(array(
+            'id' => 0,
+        ), $atts, 'studiou_free_photo_product');
+
+        $product_id = absint($atts['id']);
+        if (!$product_id) {
+            return '<p class="studiou-fpp-error">' . esc_html__('Product ID is required.', 'studiou-wc-free-photo-product') . '</p>';
+        }
+
+        $product = wc_get_product($product_id);
+        if (!$product) {
+            return '<p class="studiou-fpp-error">' . esc_html__('Product not found.', 'studiou-wc-free-photo-product') . '</p>';
+        }
+
+        if (!Studiou_WC_FPP_Product::is_fpp_product($product_id)) {
+            return '<p class="studiou-fpp-error">' . esc_html__('This product does not support photo upload.', 'studiou-wc-free-photo-product') . '</p>';
+        }
+
+        if (!$product->is_type('variable')) {
+            return '<p class="studiou-fpp-error">' . esc_html__('This product must be a variable product.', 'studiou-wc-free-photo-product') . '</p>';
+        }
+
+        // Enqueue assets
+        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);
+
+        ob_start();
+        include STUDIOU_WCFPP_PLUGIN_DIR . 'views/frontend-product.php';
+        return ob_get_clean();
+    }
+}

+ 291 - 0
studiou-wc-free-photo-product/includes/class-studiou-wc-fpp-upload.php

@@ -0,0 +1,291 @@
+<?php
+if (!defined('WPINC')) {
+    die;
+}
+
+class Studiou_WC_FPP_Upload {
+
+    /** @var Studiou_WC_FPP_DB */
+    private $db;
+
+    public function __construct($db) {
+        $this->db = $db;
+        add_action('wp_ajax_studiou_wcfpp_upload_chunk', array($this, 'handle_chunk_upload'));
+        add_action('wp_ajax_nopriv_studiou_wcfpp_upload_chunk', array($this, 'handle_chunk_upload'));
+        add_action('wp_ajax_studiou_wcfpp_remove_upload', array($this, 'handle_remove_upload'));
+        add_action('wp_ajax_nopriv_studiou_wcfpp_remove_upload', array($this, 'handle_remove_upload'));
+    }
+
+    private function get_chunks_dir() {
+        $upload_dir = wp_upload_dir();
+        return $upload_dir['basedir'] . '/studiou-fpp-chunks';
+    }
+
+    private function get_allowed_mime_types() {
+        return array(
+            'jpg|jpeg|jpe' => 'image/jpeg',
+            'png'          => 'image/png',
+            'tiff|tif'     => 'image/tiff',
+            'bmp'          => 'image/bmp',
+            'psd'          => 'image/vnd.adobe.photoshop',
+            'cr2'          => 'image/x-canon-cr2',
+            'nef'          => 'image/x-nikon-nef',
+            'arw'          => 'image/x-sony-arw',
+            'dng'          => 'image/x-adobe-dng',
+            'orf'          => 'image/x-olympus-orf',
+            'rw2'          => 'image/x-panasonic-rw2',
+            'raf'          => 'image/x-fuji-raf',
+            'webp'         => 'image/webp',
+        );
+    }
+
+    public function handle_chunk_upload() {
+        // Clean output buffers
+        while (ob_get_level()) {
+            ob_end_clean();
+        }
+
+        check_ajax_referer('studiou-wcfpp-front-nonce', 'nonce');
+
+        $product_id  = isset($_POST['product_id']) ? absint($_POST['product_id']) : 0;
+        $upload_id   = isset($_POST['upload_id']) ? sanitize_text_field($_POST['upload_id']) : '';
+        $chunk_index = isset($_POST['chunk_index']) ? absint($_POST['chunk_index']) : 0;
+        $total_chunks = isset($_POST['total_chunks']) ? absint($_POST['total_chunks']) : 0;
+        $file_name   = isset($_POST['file_name']) ? sanitize_file_name($_POST['file_name']) : '';
+        $file_size   = isset($_POST['file_size']) ? absint($_POST['file_size']) : 0;
+
+        // Validate product
+        if (!$product_id || !Studiou_WC_FPP_Product::is_fpp_product($product_id)) {
+            wp_send_json_error(array('message' => __('Invalid product.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        // Validate file size
+        $max_size = Studiou_WC_FPP_Product::get_product_max_file_size($product_id);
+        if ($file_size > ($max_size * 1024 * 1024)) {
+            wp_send_json_error(array('message' => sprintf(
+                __('File is too large. Maximum size: %s MB', 'studiou-wc-free-photo-product'),
+                $max_size
+            )));
+            return;
+        }
+
+        // Validate upload_id format (should be alphanumeric)
+        if (!preg_match('/^[a-zA-Z0-9_-]+$/', $upload_id)) {
+            wp_send_json_error(array('message' => __('Invalid upload ID.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        // Validate chunk file exists
+        if (!isset($_FILES['chunk']) || $_FILES['chunk']['error'] !== UPLOAD_ERR_OK) {
+            wp_send_json_error(array('message' => __('Chunk upload failed.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        $chunks_dir = $this->get_chunks_dir();
+        if (!file_exists($chunks_dir)) {
+            wp_mkdir_p($chunks_dir);
+        }
+
+        // Store the chunk
+        $chunk_file = $chunks_dir . '/' . $upload_id . '_chunk_' . $chunk_index;
+        if (!move_uploaded_file($_FILES['chunk']['tmp_name'], $chunk_file)) {
+            wp_send_json_error(array('message' => __('Failed to store chunk.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        // If this is the last chunk, assemble the file
+        if ($chunk_index === $total_chunks - 1) {
+            $result = $this->assemble_chunks($upload_id, $total_chunks, $file_name, $product_id);
+            if (is_wp_error($result)) {
+                wp_send_json_error(array('message' => $result->get_error_message()));
+                return;
+            }
+
+            wp_send_json_success(array(
+                'complete'      => true,
+                'attachment_id' => $result['attachment_id'],
+                'file_record_id' => $result['file_record_id'],
+                'thumbnail_url' => $result['thumbnail_url'],
+                'file_name'     => $result['file_name'],
+            ));
+            return;
+        }
+
+        wp_send_json_success(array(
+            'complete'    => false,
+            'chunk_index' => $chunk_index,
+        ));
+    }
+
+    private function assemble_chunks($upload_id, $total_chunks, $file_name, $product_id) {
+        $chunks_dir = $this->get_chunks_dir();
+        $upload_dir = wp_upload_dir();
+
+        // Create a subdirectory for free photo uploads
+        $fpp_dir = $upload_dir['path'] . '/free-photo';
+        if (!file_exists($fpp_dir)) {
+            wp_mkdir_p($fpp_dir);
+        }
+
+        // Generate unique filename
+        $ext = pathinfo($file_name, PATHINFO_EXTENSION);
+        $base = sanitize_file_name(pathinfo($file_name, PATHINFO_FILENAME));
+        $unique_name = $base . '_' . uniqid() . '.' . $ext;
+        $assembled_path = $fpp_dir . '/' . $unique_name;
+
+        // Assemble chunks
+        $output = fopen($assembled_path, 'wb');
+        if (!$output) {
+            $this->cleanup_chunks($upload_id, $total_chunks);
+            return new WP_Error('assemble_failed', __('Failed to create output file.', 'studiou-wc-free-photo-product'));
+        }
+
+        for ($i = 0; $i < $total_chunks; $i++) {
+            $chunk_file = $chunks_dir . '/' . $upload_id . '_chunk_' . $i;
+            if (!file_exists($chunk_file)) {
+                fclose($output);
+                unlink($assembled_path);
+                $this->cleanup_chunks($upload_id, $total_chunks);
+                return new WP_Error('chunk_missing', sprintf(
+                    __('Missing chunk %d.', 'studiou-wc-free-photo-product'),
+                    $i
+                ));
+            }
+
+            $chunk_data = file_get_contents($chunk_file);
+            fwrite($output, $chunk_data);
+        }
+        fclose($output);
+
+        // Clean up chunk files
+        $this->cleanup_chunks($upload_id, $total_chunks);
+
+        // Validate file extension
+        $allowed = $this->get_allowed_mime_types();
+        $ext_lower = strtolower($ext);
+        $valid = false;
+        foreach ($allowed as $exts => $mime) {
+            $ext_list = explode('|', $exts);
+            if (in_array($ext_lower, $ext_list)) {
+                $valid = true;
+                break;
+            }
+        }
+        if (!$valid) {
+            unlink($assembled_path);
+            return new WP_Error('invalid_type', __('File type not allowed.', 'studiou-wc-free-photo-product'));
+        }
+
+        // Create WP attachment
+        $relative_path = str_replace($upload_dir['basedir'] . '/', '', $assembled_path);
+        $filetype = wp_check_filetype($unique_name, $allowed);
+
+        $attachment_data = array(
+            'post_mime_type' => $filetype['type'] ?: 'application/octet-stream',
+            'post_title'     => sanitize_file_name($file_name),
+            'post_content'   => '',
+            'post_status'    => 'inherit',
+            'post_parent'    => $product_id,
+        );
+
+        $attachment_id = wp_insert_attachment($attachment_data, $assembled_path, $product_id);
+        if (is_wp_error($attachment_id)) {
+            unlink($assembled_path);
+            return $attachment_id;
+        }
+
+        // Generate metadata
+        require_once(ABSPATH . 'wp-admin/includes/image.php');
+        $metadata = wp_generate_attachment_metadata($attachment_id, $assembled_path);
+        wp_update_attachment_metadata($attachment_id, $metadata);
+
+        // Assign media category
+        $media_cat_id = Studiou_WC_FPP_Product::get_product_media_category($product_id);
+        if ($media_cat_id) {
+            wp_set_object_terms($attachment_id, array((int) $media_cat_id), 'studiou_media_category');
+        }
+
+        // Get thumbnail URL
+        $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 session key for guests
+        $session_key = '';
+        if (!is_user_logged_in()) {
+            if (WC()->session) {
+                $session_key = WC()->session->get_customer_id();
+            }
+        }
+
+        // Insert file record
+        $file_record_id = $this->db->insert_file_record(array(
+            'product_id'    => $product_id,
+            'attachment_id' => $attachment_id,
+            'customer_id'   => get_current_user_id(),
+            'session_key'   => $session_key,
+            'file_name'     => $file_name,
+        ));
+
+        if (!$file_record_id) {
+            wp_delete_attachment($attachment_id, true);
+            return new WP_Error('record_failed', __('Failed to create file record.', 'studiou-wc-free-photo-product'));
+        }
+
+        return array(
+            'attachment_id'  => $attachment_id,
+            'file_record_id' => $file_record_id,
+            'thumbnail_url'  => $thumbnail_url,
+            'file_name'      => $file_name,
+        );
+    }
+
+    private function cleanup_chunks($upload_id, $total_chunks) {
+        $chunks_dir = $this->get_chunks_dir();
+        for ($i = 0; $i < $total_chunks; $i++) {
+            $chunk_file = $chunks_dir . '/' . $upload_id . '_chunk_' . $i;
+            if (file_exists($chunk_file)) {
+                unlink($chunk_file);
+            }
+        }
+    }
+
+    public function handle_remove_upload() {
+        check_ajax_referer('studiou-wcfpp-front-nonce', 'nonce');
+
+        $file_record_id = isset($_POST['file_record_id']) ? absint($_POST['file_record_id']) : 0;
+        if (!$file_record_id) {
+            wp_send_json_error(array('message' => __('Invalid file.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        $record = $this->db->get_file_record($file_record_id);
+        if (!$record) {
+            wp_send_json_error(array('message' => __('File not found.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        // Only allow removal if not yet linked to an order
+        if ($record->order_id > 0) {
+            wp_send_json_error(array('message' => __('Cannot remove a file linked to an order.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        // Verify ownership
+        $current_user_id = get_current_user_id();
+        if ($current_user_id > 0 && (int) $record->customer_id !== $current_user_id) {
+            wp_send_json_error(array('message' => __('Permission denied.', 'studiou-wc-free-photo-product')));
+            return;
+        }
+
+        $this->db->delete_file_record($file_record_id);
+
+        wp_send_json_success(array('message' => __('File removed.', 'studiou-wc-free-photo-product')));
+    }
+}

+ 146 - 0
studiou-wc-free-photo-product/languages/generate-mo.php

@@ -0,0 +1,146 @@
+<?php
+/**
+ * Generate .mo files from .po files.
+ * Run: php generate-mo.php
+ * Or use Poedit / WP-CLI: wp i18n make-mo .
+ */
+
+if (php_sapi_name() !== 'cli') {
+    die('CLI only');
+}
+
+$dir = __DIR__;
+$po_files = glob($dir . '/*.po');
+
+foreach ($po_files as $po_file) {
+    $mo_file = preg_replace('/\.po$/', '.mo', $po_file);
+    echo "Converting: " . basename($po_file) . " -> " . basename($mo_file) . "\n";
+
+    $entries = parse_po($po_file);
+    write_mo($mo_file, $entries);
+    echo "Done. " . count($entries) . " entries.\n";
+}
+
+function parse_po($file) {
+    $content = file_get_contents($file);
+    $entries = array();
+    $current_msgid = null;
+    $current_msgstr = array();
+    $current_msgid_plural = null;
+    $state = '';
+
+    $lines = explode("\n", $content);
+    foreach ($lines as $line) {
+        $line = trim($line);
+
+        // Skip comments
+        if (empty($line) || $line[0] === '#') {
+            if ($current_msgid !== null && $state !== '') {
+                save_entry($entries, $current_msgid, $current_msgid_plural, $current_msgstr);
+                $current_msgid = null;
+                $current_msgid_plural = null;
+                $current_msgstr = array();
+                $state = '';
+            }
+            continue;
+        }
+
+        if (preg_match('/^msgid\s+"(.*)"$/', $line, $m)) {
+            if ($current_msgid !== null) {
+                save_entry($entries, $current_msgid, $current_msgid_plural, $current_msgstr);
+            }
+            $current_msgid = stripcslashes($m[1]);
+            $current_msgid_plural = null;
+            $current_msgstr = array();
+            $state = 'msgid';
+        } elseif (preg_match('/^msgid_plural\s+"(.*)"$/', $line, $m)) {
+            $current_msgid_plural = stripcslashes($m[1]);
+            $state = 'msgid_plural';
+        } elseif (preg_match('/^msgstr\s+"(.*)"$/', $line, $m)) {
+            $current_msgstr[0] = stripcslashes($m[1]);
+            $state = 'msgstr';
+        } elseif (preg_match('/^msgstr\[(\d+)\]\s+"(.*)"$/', $line, $m)) {
+            $current_msgstr[(int)$m[1]] = stripcslashes($m[2]);
+            $state = 'msgstr[' . $m[1] . ']';
+        } elseif (preg_match('/^"(.*)"$/', $line, $m)) {
+            $val = stripcslashes($m[1]);
+            if ($state === 'msgid') {
+                $current_msgid .= $val;
+            } elseif ($state === 'msgid_plural') {
+                $current_msgid_plural .= $val;
+            } elseif (preg_match('/^msgstr/', $state)) {
+                $idx = 0;
+                if (preg_match('/\[(\d+)\]/', $state, $im)) {
+                    $idx = (int)$im[1];
+                }
+                $current_msgstr[$idx] = ($current_msgstr[$idx] ?? '') . $val;
+            }
+        }
+    }
+
+    if ($current_msgid !== null) {
+        save_entry($entries, $current_msgid, $current_msgid_plural, $current_msgstr);
+    }
+
+    return $entries;
+}
+
+function save_entry(&$entries, $msgid, $msgid_plural, $msgstr) {
+    if ($msgid === null) return;
+
+    if ($msgid_plural !== null) {
+        $key = $msgid . "\x00" . $msgid_plural;
+        ksort($msgstr);
+        $val = implode("\x00", $msgstr);
+    } else {
+        $key = $msgid;
+        $val = $msgstr[0] ?? '';
+    }
+
+    $entries[$key] = $val;
+}
+
+function write_mo($file, $entries) {
+    ksort($entries);
+    $offsets = array();
+    $ids = '';
+    $strs = '';
+
+    foreach ($entries as $id => $str) {
+        $offsets[] = array(strlen($id), strlen($ids), strlen($str), strlen($strs));
+        $ids .= $id . "\x00";
+        $strs .= $str . "\x00";
+    }
+
+    $count = count($offsets);
+    $header_size = 28;
+    $key_offset_start = $header_size;
+    $val_offset_start = $key_offset_start + $count * 8;
+    $keys_start = $val_offset_start + $count * 8;
+    $vals_start = $keys_start + strlen($ids);
+
+    $output = pack('V', 0x950412de);  // magic
+    $output .= pack('V', 0);          // revision
+    $output .= pack('V', $count);     // number of strings
+    $output .= pack('V', $key_offset_start);  // offset of original strings table
+    $output .= pack('V', $val_offset_start);  // offset of translated strings table
+    $output .= pack('V', 0);          // hash table size
+    $output .= pack('V', 0);          // hash table offset
+
+    // Original strings offsets
+    foreach ($offsets as $o) {
+        $output .= pack('V', $o[0]);                // length
+        $output .= pack('V', $keys_start + $o[1]);  // offset
+    }
+
+    // Translated strings offsets
+    foreach ($offsets as $o) {
+        $output .= pack('V', $o[2]);                // length
+        $output .= pack('V', $vals_start + $o[3]);  // offset
+    }
+
+    $output .= $ids;
+    $output .= $strs;
+
+    file_put_contents($file, $output);
+}

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

@@ -0,0 +1,335 @@
+# Czech translation for QDR - Studiou WC Free Photo Product
+# Copyright (C) 2026 Dalibor Votruba
+# 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"
+"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"
+"Last-Translator: Dalibor Votruba <info@quadarax.com>\n"
+"Language-Team: Czech <info@quadarax.com>\n"
+"Language: cs_CZ\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n>=2 && n<=4 ? 1 : 2);\n"
+"X-Generator: Poedit 3.0\n"
+
+msgid "QDR - Studiou WC Free Photo Product requires WooCommerce to be installed and activated."
+msgstr "QDR - Studiou WC Free Photo Product vyžaduje nainstalovaný a aktivovaný WooCommerce."
+
+msgid "Free Photo Files"
+msgstr "Soubory fotografií"
+
+msgid "Are you sure you want to delete the selected files?"
+msgstr "Opravdu chcete smazat vybrané soubory?"
+
+msgid "Are you sure you want to delete this file?"
+msgstr "Opravdu chcete smazat tento soubor?"
+
+msgid "No files selected."
+msgstr "Nejsou vybrány žádné soubory."
+
+msgid "Status updated."
+msgstr "Stav aktualizován."
+
+msgid "An error occurred."
+msgstr "Došlo k chybě."
+
+msgid "Generating ZIP archive..."
+msgstr "Generování ZIP archivu..."
+
+msgid "Deleting..."
+msgstr "Mazání..."
+
+msgid "Uploading..."
+msgstr "Nahrávání..."
+
+msgid "Upload complete"
+msgstr "Nahrávání dokončeno"
+
+msgid "Upload failed. Please try again."
+msgstr "Nahrávání selhalo. Zkuste to prosím znovu."
+
+msgid "File is too large. Maximum size: %s MB"
+msgstr "Soubor je příliš velký. Maximální velikost: %s MB"
+
+msgid "Please select a variant."
+msgstr "Prosím vyberte variantu."
+
+msgid "Please upload a file."
+msgstr "Prosím nahrajte soubor."
+
+msgid "Product added to cart!"
+msgstr "Produkt přidán do košíku!"
+
+msgid "Could not add to cart. Please try again."
+msgstr "Nelze přidat do košíku. Zkuste to prosím znovu."
+
+msgid "Drag & drop your file here or click to browse"
+msgstr "Přetáhněte soubor sem nebo klikněte pro výběr"
+
+msgid "Remove"
+msgstr "Odebrat"
+
+msgid "Media Categories"
+msgstr "Kategorie médií"
+
+msgid "Media Category"
+msgstr "Kategorie médií"
+
+msgid "Search Media Categories"
+msgstr "Hledat kategorie médií"
+
+msgid "All Media Categories"
+msgstr "Všechny kategorie médií"
+
+msgid "Parent Media Category"
+msgstr "Nadřazená kategorie médií"
+
+msgid "Parent Media Category:"
+msgstr "Nadřazená kategorie médií:"
+
+msgid "Edit Media Category"
+msgstr "Upravit kategorii médií"
+
+msgid "Update Media Category"
+msgstr "Aktualizovat kategorii médií"
+
+msgid "Add New Media Category"
+msgstr "Přidat novou kategorii médií"
+
+msgid "New Media Category Name"
+msgstr "Název nové kategorie médií"
+
+msgid "Free Photo"
+msgstr "Volná fotografie"
+
+msgid "Enable Free Photo"
+msgstr "Povolit volnou fotografii"
+
+msgid "Enable custom photo upload for this product"
+msgstr "Povolit nahrávání vlastních fotografií pro tento produkt"
+
+msgid "— Select category —"
+msgstr "— Vyberte kategorii —"
+
+msgid "Uploaded files will be assigned to this media category."
+msgstr "Nahrané soubory budou přiřazeny k této kategorii médií."
+
+msgid "New Category Name"
+msgstr "Název nové kategorie"
+
+msgid "Add"
+msgstr "Přidat"
+
+msgid "Cancel"
+msgstr "Zrušit"
+
+msgid "Max File Size (MB)"
+msgstr "Max. velikost souboru (MB)"
+
+msgid "Maximum allowed file size for uploads in megabytes."
+msgstr "Maximální povolená velikost souboru pro nahrávání v megabajtech."
+
+msgid "Shortcode"
+msgstr "Shortcode"
+
+msgid "Use this shortcode to display this product with photo upload on any page."
+msgstr "Použijte tento shortcode pro zobrazení tohoto produktu s nahráváním fotografií na jakékoli stránce."
+
+msgid "Category name is required."
+msgstr "Název kategorie je povinný."
+
+msgid "Insufficient permissions."
+msgstr "Nedostatečná oprávnění."
+
+msgid "Invalid product."
+msgstr "Neplatný produkt."
+
+msgid "Invalid upload ID."
+msgstr "Neplatné ID nahrávání."
+
+msgid "Chunk upload failed."
+msgstr "Nahrávání části selhalo."
+
+msgid "Failed to store chunk."
+msgstr "Nepodařilo se uložit část souboru."
+
+msgid "Failed to create output file."
+msgstr "Nepodařilo se vytvořit výstupní soubor."
+
+msgid "Missing chunk %d."
+msgstr "Chybějící část %d."
+
+msgid "File type not allowed."
+msgstr "Tento typ souboru není povolen."
+
+msgid "Failed to create file record."
+msgstr "Nepodařilo se vytvořit záznam souboru."
+
+msgid "Invalid file."
+msgstr "Neplatný soubor."
+
+msgid "File not found."
+msgstr "Soubor nenalezen."
+
+msgid "Cannot remove a file linked to an order."
+msgstr "Nelze odstranit soubor propojený s objednávkou."
+
+msgid "Permission denied."
+msgstr "Přístup zamítnut."
+
+msgid "File removed."
+msgstr "Soubor odstraněn."
+
+msgid "Product ID is required."
+msgstr "ID produktu je povinné."
+
+msgid "Product not found."
+msgstr "Produkt nenalezen."
+
+msgid "This product does not support photo upload."
+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 "Could not add to cart."
+msgstr "Nelze přidat do košíku."
+
+msgid "Uploaded Photo"
+msgstr "Nahraná fotografie"
+
+msgid "Select Variant"
+msgstr "Vyberte variantu"
+
+msgid "Price"
+msgstr "Cena"
+
+msgid "Upload Your Photo"
+msgstr "Nahrajte svou fotografii"
+
+msgid "Maximum file size: %s MB. Accepted formats: JPEG, PNG, TIFF, BMP, PSD, RAW (CR2, NEF, ARW, DNG, ORF, RW2, RAF), WebP."
+msgstr "Maximální velikost souboru: %s MB. Povolené formáty: JPEG, PNG, TIFF, BMP, PSD, RAW (CR2, NEF, ARW, DNG, ORF, RW2, RAF), WebP."
+
+msgid "Preview"
+msgstr "Náhled"
+
+msgid "Add to Cart"
+msgstr "Přidat do košíku"
+
+msgid "All Statuses"
+msgstr "Všechny stavy"
+
+msgid "Ready"
+msgstr "Připraveno"
+
+msgid "Downloaded"
+msgstr "Staženo"
+
+msgid "Solved"
+msgstr "Vyřízeno"
+
+msgid "All Products"
+msgstr "Všechny produkty"
+
+msgid "Search file name..."
+msgstr "Hledat název souboru..."
+
+msgid "Filter"
+msgstr "Filtrovat"
+
+msgid "Bulk Actions"
+msgstr "Hromadné akce"
+
+msgid "Download ZIP"
+msgstr "Stáhnout ZIP"
+
+msgid "Set Status: Ready"
+msgstr "Nastavit stav: Připraveno"
+
+msgid "Set Status: Downloaded"
+msgstr "Nastavit stav: Staženo"
+
+msgid "Set Status: Solved"
+msgstr "Nastavit stav: Vyřízeno"
+
+msgid "Delete"
+msgstr "Smazat"
+
+msgid "Apply"
+msgstr "Použít"
+
+msgid "%d file"
+msgid_plural "%d files"
+msgstr[0] "%d soubor"
+msgstr[1] "%d soubory"
+msgstr[2] "%d souborů"
+
+msgid "File Name"
+msgstr "Název souboru"
+
+msgid "Product"
+msgstr "Produkt"
+
+msgid "Order"
+msgstr "Objednávka"
+
+msgid "Customer"
+msgstr "Zákazník"
+
+msgid "Status"
+msgstr "Stav"
+
+msgid "Date"
+msgstr "Datum"
+
+msgid "Actions"
+msgstr "Akce"
+
+msgid "No files found."
+msgstr "Žádné soubory nenalezeny."
+
+msgid "Guest"
+msgstr "Host"
+
+msgid "Download"
+msgstr "Stáhnout"
+
+msgid "Invalid data."
+msgstr "Neplatná data."
+
+msgid "Failed to update status."
+msgstr "Nepodařilo se aktualizovat stav."
+
+msgid "%d file(s) updated."
+msgstr "Aktualizováno %d soubor(ů)."
+
+msgid "%d file(s) deleted."
+msgstr "Smazáno %d soubor(ů)."
+
+msgid "ZIP extension not available on server."
+msgstr "Rozšíření ZIP není na serveru dostupné."
+
+msgid "Could not create ZIP file."
+msgstr "Nelze vytvořit ZIP soubor."
+
+msgid "No files could be added to archive."
+msgstr "Do archivu nebylo možné přidat žádné soubory."
+
+msgid "Security check failed."
+msgstr "Bezpečnostní kontrola selhala."
+
+msgid "Add New Category"
+msgstr "Přidat novou kategorii"

+ 444 - 0
studiou-wc-free-photo-product/languages/studiou-wc-free-photo-product.pot

@@ -0,0 +1,444 @@
+# Copyright (C) 2026 Dalibor Votruba
+# 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"
+"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"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: studiou-wc-free-photo-product.php
+msgid "QDR - Studiou WC Free Photo Product requires WooCommerce to be installed and activated."
+msgstr ""
+
+#: studiou-wc-free-photo-product.php
+msgid "Free Photo Files"
+msgstr ""
+
+#: studiou-wc-free-photo-product.php
+msgid "Are you sure you want to delete the selected files?"
+msgstr ""
+
+#: studiou-wc-free-photo-product.php
+msgid "Are you sure you want to delete this file?"
+msgstr ""
+
+#: studiou-wc-free-photo-product.php
+msgid "No files selected."
+msgstr ""
+
+#: studiou-wc-free-photo-product.php
+msgid "Status updated."
+msgstr ""
+
+#: studiou-wc-free-photo-product.php
+msgid "An error occurred."
+msgstr ""
+
+#: studiou-wc-free-photo-product.php
+msgid "Generating ZIP archive..."
+msgstr ""
+
+#: studiou-wc-free-photo-product.php
+msgid "Deleting..."
+msgstr ""
+
+#: studiou-wc-free-photo-product.php
+msgid "Uploading..."
+msgstr ""
+
+#: studiou-wc-free-photo-product.php
+msgid "Upload complete"
+msgstr ""
+
+#: studiou-wc-free-photo-product.php
+msgid "Upload failed. Please try again."
+msgstr ""
+
+#: studiou-wc-free-photo-product.php
+msgid "File is too large. Maximum size: %s MB"
+msgstr ""
+
+#: studiou-wc-free-photo-product.php
+msgid "Please select a variant."
+msgstr ""
+
+#: studiou-wc-free-photo-product.php
+msgid "Please upload a file."
+msgstr ""
+
+#: studiou-wc-free-photo-product.php
+msgid "Product added to cart!"
+msgstr ""
+
+#: studiou-wc-free-photo-product.php
+msgid "Could not add to cart. Please try again."
+msgstr ""
+
+#: studiou-wc-free-photo-product.php
+msgid "Drag & drop your file here or click to browse"
+msgstr ""
+
+#: studiou-wc-free-photo-product.php
+msgid "Remove"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-db.php
+msgid "Media Categories"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-db.php
+msgid "Media Category"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-db.php
+msgid "Search Media Categories"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-db.php
+msgid "All Media Categories"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-db.php
+msgid "Parent Media Category"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-db.php
+msgid "Parent Media Category:"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-db.php
+msgid "Edit Media Category"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-db.php
+msgid "Update Media Category"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-db.php
+msgid "Add New Media Category"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-db.php
+msgid "New Media Category Name"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-product.php
+msgid "Free Photo"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-product.php
+msgid "Enable Free Photo"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-product.php
+msgid "Enable custom photo upload for this product"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-product.php
+msgid "— Select category —"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-product.php
+msgid "Uploaded files will be assigned to this media category."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-product.php
+msgid "New Category Name"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-product.php
+msgid "Add"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-product.php
+msgid "Cancel"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-product.php
+msgid "Max File Size (MB)"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-product.php
+msgid "Maximum allowed file size for uploads in megabytes."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-product.php
+msgid "Shortcode"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-product.php
+msgid "Use this shortcode to display this product with photo upload on any page."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-product.php
+msgid "Category name is required."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-product.php
+msgid "Insufficient permissions."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-upload.php
+msgid "Invalid product."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-upload.php
+msgid "Invalid upload ID."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-upload.php
+msgid "Chunk upload failed."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-upload.php
+msgid "Failed to store chunk."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-upload.php
+msgid "Failed to create output file."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-upload.php
+msgid "Missing chunk %d."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-upload.php
+msgid "File type not allowed."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-upload.php
+msgid "Failed to create file record."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-upload.php
+msgid "Invalid file."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-upload.php
+msgid "File not found."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-upload.php
+msgid "Cannot remove a file linked to an order."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-upload.php
+msgid "Permission denied."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-upload.php
+msgid "File removed."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-shortcode.php
+msgid "Product ID is required."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-shortcode.php
+msgid "Product not found."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-shortcode.php
+msgid "This product does not support photo upload."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-shortcode.php
+msgid "This product must be a variable product."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-cart.php
+msgid "Missing required data."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-cart.php
+msgid "Invalid variant."
+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."
+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
+msgid "Upload Your Photo"
+msgstr ""
+
+#: views/frontend-product.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
+msgid "Preview"
+msgstr ""
+
+#: views/frontend-product.php
+msgid "Add to Cart"
+msgstr ""
+
+#: views/admin-summary.php
+msgid "All Statuses"
+msgstr ""
+
+#: views/admin-summary.php
+msgid "Ready"
+msgstr ""
+
+#: views/admin-summary.php
+msgid "Downloaded"
+msgstr ""
+
+#: views/admin-summary.php
+msgid "Solved"
+msgstr ""
+
+#: views/admin-summary.php
+msgid "All Products"
+msgstr ""
+
+#: views/admin-summary.php
+msgid "Search file name..."
+msgstr ""
+
+#: views/admin-summary.php
+msgid "Filter"
+msgstr ""
+
+#: views/admin-summary.php
+msgid "Bulk Actions"
+msgstr ""
+
+#: views/admin-summary.php
+msgid "Download ZIP"
+msgstr ""
+
+#: views/admin-summary.php
+msgid "Set Status: Ready"
+msgstr ""
+
+#: views/admin-summary.php
+msgid "Set Status: Downloaded"
+msgstr ""
+
+#: views/admin-summary.php
+msgid "Set Status: Solved"
+msgstr ""
+
+#: views/admin-summary.php
+msgid "Delete"
+msgstr ""
+
+#: views/admin-summary.php
+msgid "Apply"
+msgstr ""
+
+#: views/admin-summary.php
+#, php-format
+msgid "%d file"
+msgid_plural "%d files"
+msgstr[0] ""
+msgstr[1] ""
+
+#: views/admin-summary.php
+msgid "File Name"
+msgstr ""
+
+#: views/admin-summary.php
+msgid "Product"
+msgstr ""
+
+#: views/admin-summary.php
+msgid "Order"
+msgstr ""
+
+#: views/admin-summary.php
+msgid "Customer"
+msgstr ""
+
+#: views/admin-summary.php
+msgid "Status"
+msgstr ""
+
+#: views/admin-summary.php
+msgid "Date"
+msgstr ""
+
+#: views/admin-summary.php
+msgid "Actions"
+msgstr ""
+
+#: views/admin-summary.php
+msgid "No files found."
+msgstr ""
+
+#: views/admin-summary.php
+msgid "Guest"
+msgstr ""
+
+#: views/admin-summary.php
+msgid "Download"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-admin.php
+msgid "Invalid data."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-admin.php
+msgid "Failed to update status."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-admin.php
+#, php-format
+msgid "%d file(s) updated."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-admin.php
+#, php-format
+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 ""
+
+#: includes/class-studiou-wc-fpp-admin.php
+msgid "Could not create ZIP file."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-admin.php
+msgid "No files could be added to archive."
+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 ""

+ 202 - 0
studiou-wc-free-photo-product/readme.md

@@ -0,0 +1,202 @@
+# 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.
+
+## Requirements
+
+- WordPress 6.9.4+
+- WooCommerce 10.0+
+- PHP 8.2+
+- PHP ZipArchive extension (for admin ZIP download)
+
+## Installation
+
+1. Upload the `studiou-wc-free-photo-product` folder to `/wp-content/plugins/`
+2. Activate the plugin through the Plugins menu in WordPress
+3. The plugin creates a custom database table `{prefix}studiou_fpp_files` on activation
+
+## Configuration
+
+### Creating a Free Photo Product
+
+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
+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
+
+### Displaying on Frontend
+
+Use the shortcode on any page or post:
+
+```
+[studiou_free_photo_product id="123"]
+```
+
+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)
+
+### Accepted File Formats
+
+JPEG, PNG, TIFF, BMP, PSD, WebP, and camera RAW formats: CR2 (Canon), NEF (Nikon), ARW (Sony), DNG (Adobe), ORF (Olympus), RW2 (Panasonic), RAF (Fuji).
+
+## Chunked Upload
+
+Files are uploaded in 1 MB chunks via AJAX to bypass PHP `upload_max_filesize` and `post_max_size` limits. The server assembles chunks into the final file, creates a WordPress media attachment, and assigns it to the configured media category. The chunk size is defined by the `STUDIOU_WCFPP_CHUNK_SIZE` constant.
+
+## Admin Summary Page
+
+Navigate to **Products > Free Photo Files** to manage uploaded files.
+
+### Features
+
+- Filter by status (Ready / Downloaded / Solved), product, or file name search
+- Inline status change per file
+- Single file download link
+- Single file delete with media attachment cleanup
+- Bulk actions: Download as ZIP, Set Status, Delete
+- Order number links (HPOS-compatible)
+- Customer name display
+- Pagination (30 files per page)
+
+### File Statuses
+
+| Status | Meaning |
+|---|---|
+| Ready | File uploaded, awaiting processing |
+| Downloaded | File has been downloaded by admin |
+| Solved | File has been printed / order fulfilled |
+
+## Cart and Order Integration
+
+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
+- In the order admin, the uploaded photo file name appears as order item metadata
+- Files linked to orders cannot be removed by the customer
+
+## 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)
+- Media Library sidebar
+
+Each product has one assigned media category. All files uploaded for that product are tagged with it.
+
+## HPOS Support
+
+The plugin declares full compatibility with WooCommerce High-Performance Order Storage (HPOS / Custom Order Tables). Order edit links in the admin summary page use the correct URL format based on whether HPOS is enabled.
+
+## Localization
+
+Supported languages:
+- English (default)
+- Czech (cs_CZ)
+
+Translation files are in the `languages/` directory.
+
+To compile the `.mo` file from `.po`:
+
+```bash
+# Using the included helper script
+php languages/generate-mo.php
+
+# Or using WP-CLI
+wp i18n make-mo languages/
+
+# Or using gettext
+msgfmt languages/studiou-wc-free-photo-product-cs_CZ.po -o languages/studiou-wc-free-photo-product-cs_CZ.mo
+```
+
+Text domain: `studiou-wc-free-photo-product`
+
+## Database
+
+The plugin creates one custom table on activation:
+
+**`{prefix}studiou_fpp_files`**
+
+| Column | Type | Description |
+|---|---|---|
+| id | bigint | Primary key |
+| product_id | bigint | WC product ID |
+| variation_id | bigint | WC variation ID |
+| order_id | bigint | WC order ID (set on checkout) |
+| order_item_id | bigint | WC order item ID (set on checkout) |
+| attachment_id | bigint | WP media attachment ID |
+| customer_id | bigint | WP user ID (0 for guests) |
+| session_key | varchar(64) | WC session key for guest users |
+| file_name | varchar(255) | Original uploaded file name |
+| status | varchar(20) | ready / downloaded / solved |
+| created_at | datetime | Upload timestamp |
+| updated_at | datetime | Last update timestamp |
+
+## File Structure
+
+```
+studiou-wc-free-photo-product/
+├── 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
+├── views/
+│   ├── frontend-product.php            Shortcode template
+│   └── 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)
+└── 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
+```
+
+## Hooks and Filters
+
+### AJAX Actions
+
+| Action | Auth | Description |
+|---|---|---|
+| `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_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 |
+
+### Product Meta Keys
+
+| Key | Description |
+|---|---|
+| `_studiou_fpp_enabled` | "yes" / "no" |
+| `_studiou_fpp_media_category` | Term ID of media category |
+| `_studiou_fpp_max_file_size` | Max upload size in MB |
+
+### Order Item Meta Keys
+
+| Key | Description |
+|---|---|
+| `_studiou_fpp_attachment_id` | WP attachment ID |
+| `_studiou_fpp_file_record_id` | File record ID in custom table |
+| `_studiou_fpp_file_name` | Original file name |
+
+## License
+
+GPL v2 or later
+
+## Author
+
+Dalibor Votruba - [quadarax.com](https://www.quadarax.com)

+ 257 - 0
studiou-wc-free-photo-product/studiou-wc-free-photo-product.php

@@ -0,0 +1,257 @@
+<?php
+/**
+ * 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
+ * Requires at least: 6.9.4
+ * Requires PHP: 8.2
+ * Author: Dalibor Votruba
+ * Author URI: https://www.quadarax.com
+ * License: GPL v2 or later
+ * License URI: https://www.gnu.org/licenses/gpl-2.0.html
+ * Text Domain: studiou-wc-free-photo-product
+ * Domain Path: /languages
+ * WC requires at least: 10.0
+ * WC tested up to: 10.6
+ * Woo: 12346:a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4
+ */
+
+if (!defined('WPINC')) {
+    die;
+}
+
+if (!defined('STUDIOU_WCFPP_VERSION')) define('STUDIOU_WCFPP_VERSION', '1.0.0');
+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__));
+if (!defined('STUDIOU_WCFPP_CHUNK_SIZE')) define('STUDIOU_WCFPP_CHUNK_SIZE', 1024 * 1024); // 1MB chunks
+
+class Studiou_WC_Free_Photo_Product {
+
+    /** @var Studiou_WC_FPP_DB */
+    private $db;
+
+    /** @var Studiou_WC_FPP_Product */
+    private $product;
+
+    /** @var Studiou_WC_FPP_Upload */
+    private $upload;
+
+    /** @var Studiou_WC_FPP_Shortcode */
+    private $shortcode;
+
+    /** @var Studiou_WC_FPP_Cart */
+    private $cart;
+
+    /** @var Studiou_WC_FPP_Admin */
+    private $admin;
+
+    public function __construct() {
+        $this->load_dependencies();
+        add_action('admin_init', array($this, 'check_required_plugins'));
+        add_action('admin_menu', array($this, 'add_admin_menu'), 99);
+        add_action('admin_enqueue_scripts', array($this, 'register_admin_assets'));
+        add_action('wp_enqueue_scripts', array($this, 'register_frontend_assets'));
+        $this->init_components();
+        add_action('before_woocommerce_init', array($this, 'declare_hpos_compatibility'));
+    }
+
+    private function load_dependencies() {
+        require_once STUDIOU_WCFPP_PLUGIN_DIR . 'includes/class-studiou-wc-fpp-db.php';
+        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-cart.php';
+        require_once STUDIOU_WCFPP_PLUGIN_DIR . 'includes/class-studiou-wc-fpp-admin.php';
+    }
+
+    private function init_components() {
+        $this->db = new Studiou_WC_FPP_DB();
+        $this->product = new Studiou_WC_FPP_Product();
+        $this->upload = new Studiou_WC_FPP_Upload($this->db);
+        $this->shortcode = new Studiou_WC_FPP_Shortcode();
+        $this->cart = new Studiou_WC_FPP_Cart($this->db);
+        $this->admin = new Studiou_WC_FPP_Admin($this->db);
+    }
+
+    public function check_required_plugins() {
+        if (!class_exists('WooCommerce')) {
+            add_action('admin_notices', function () {
+                echo '<div class="notice notice-error"><p>';
+                echo esc_html__('QDR - Studiou WC Free Photo Product requires WooCommerce to be installed and activated.', 'studiou-wc-free-photo-product');
+                echo '</p></div>';
+            });
+        }
+    }
+
+    public function declare_hpos_compatibility() {
+        if (class_exists('Automattic\WooCommerce\Utilities\FeaturesUtil')) {
+            \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
+                'custom_order_tables',
+                __FILE__,
+                true
+            );
+        }
+    }
+
+    public function add_admin_menu() {
+        if (class_exists('WooCommerce')) {
+            add_submenu_page(
+                'edit.php?post_type=product',
+                __('Free Photo Files', 'studiou-wc-free-photo-product'),
+                __('Free Photo Files', 'studiou-wc-free-photo-product'),
+                'manage_woocommerce',
+                'studiou-fpp-summary',
+                array($this->admin, 'render_summary_page')
+            );
+        }
+    }
+
+    public function register_admin_assets($hook) {
+        // Product edit page
+        $screen = get_current_screen();
+        if ($screen && ($screen->id === 'product' || $screen->post_type === 'product')) {
+            wp_enqueue_style(
+                'studiou-wcfpp-admin',
+                STUDIOU_WCFPP_PLUGIN_URL . 'assets/css/admin.css',
+                array(),
+                STUDIOU_WCFPP_VERSION
+            );
+            wp_enqueue_script(
+                'studiou-wcfpp-admin',
+                STUDIOU_WCFPP_PLUGIN_URL . 'assets/js/admin.js',
+                array('jquery'),
+                STUDIOU_WCFPP_VERSION,
+                true
+            );
+            wp_localize_script('studiou-wcfpp-admin', 'studiouWcfpp', array(
+                'ajaxUrl' => admin_url('admin-ajax.php'),
+                'nonce' => wp_create_nonce('studiou-wcfpp-nonce'),
+                'i18n' => array(
+                    'confirmDelete' => __('Are you sure you want to delete the selected files?', 'studiou-wc-free-photo-product'),
+                    'confirmDeleteSingle' => __('Are you sure you want to delete this file?', 'studiou-wc-free-photo-product'),
+                    'noFilesSelected' => __('No files selected.', 'studiou-wc-free-photo-product'),
+                    'statusUpdated' => __('Status updated.', 'studiou-wc-free-photo-product'),
+                    'error' => __('An error occurred.', 'studiou-wc-free-photo-product'),
+                    'generatingZip' => __('Generating ZIP archive...', 'studiou-wc-free-photo-product'),
+                    'deleting' => __('Deleting...', 'studiou-wc-free-photo-product'),
+                ),
+            ));
+        }
+
+        // Admin summary page
+        if ('product_page_studiou-fpp-summary' === $hook) {
+            wp_enqueue_style(
+                'studiou-wcfpp-admin',
+                STUDIOU_WCFPP_PLUGIN_URL . 'assets/css/admin.css',
+                array(),
+                STUDIOU_WCFPP_VERSION
+            );
+            wp_enqueue_script(
+                'studiou-wcfpp-admin',
+                STUDIOU_WCFPP_PLUGIN_URL . 'assets/js/admin.js',
+                array('jquery'),
+                STUDIOU_WCFPP_VERSION,
+                true
+            );
+            wp_localize_script('studiou-wcfpp-admin', 'studiouWcfpp', array(
+                'ajaxUrl' => admin_url('admin-ajax.php'),
+                'nonce' => wp_create_nonce('studiou-wcfpp-nonce'),
+                'i18n' => array(
+                    'confirmDelete' => __('Are you sure you want to delete the selected files?', 'studiou-wc-free-photo-product'),
+                    'confirmDeleteSingle' => __('Are you sure you want to delete this file?', 'studiou-wc-free-photo-product'),
+                    'noFilesSelected' => __('No files selected.', 'studiou-wc-free-photo-product'),
+                    'statusUpdated' => __('Status updated.', 'studiou-wc-free-photo-product'),
+                    'error' => __('An error occurred.', 'studiou-wc-free-photo-product'),
+                    'generatingZip' => __('Generating ZIP archive...', 'studiou-wc-free-photo-product'),
+                    'deleting' => __('Deleting...', 'studiou-wc-free-photo-product'),
+                ),
+            ));
+        }
+    }
+
+    public function register_frontend_assets() {
+        wp_register_style(
+            'studiou-wcfpp-frontend',
+            STUDIOU_WCFPP_PLUGIN_URL . 'assets/css/frontend.css',
+            array(),
+            STUDIOU_WCFPP_VERSION
+        );
+        wp_register_script(
+            'studiou-wcfpp-frontend',
+            STUDIOU_WCFPP_PLUGIN_URL . 'assets/js/frontend.js',
+            array('jquery'),
+            STUDIOU_WCFPP_VERSION,
+            true
+        );
+        wp_localize_script('studiou-wcfpp-frontend', 'studiouWcfppFront', array(
+            'ajaxUrl' => admin_url('admin-ajax.php'),
+            'nonce' => wp_create_nonce('studiou-wcfpp-front-nonce'),
+            'chunkSize' => STUDIOU_WCFPP_CHUNK_SIZE,
+            'cartUrl' => wc_get_cart_url(),
+            'i18n' => array(
+                'uploading' => __('Uploading...', 'studiou-wc-free-photo-product'),
+                'uploadComplete' => __('Upload complete', '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'),
+                'uploadFile' => __('Please upload a file.', 'studiou-wc-free-photo-product'),
+                'addedToCart' => __('Product added to cart!', 'studiou-wc-free-photo-product'),
+                'addToCartError' => __('Could not add to cart. Please try again.', 'studiou-wc-free-photo-product'),
+                'dragDropText' => __('Drag & drop your file here or click to browse', 'studiou-wc-free-photo-product'),
+                'removeFile' => __('Remove', 'studiou-wc-free-photo-product'),
+            ),
+        ));
+    }
+
+    public static function activate() {
+        $db = new Studiou_WC_FPP_DB();
+        $db->create_tables();
+
+        // Create chunks temp directory
+        $upload_dir = wp_upload_dir();
+        $chunks_dir = $upload_dir['basedir'] . '/studiou-fpp-chunks';
+        if (!file_exists($chunks_dir)) {
+            wp_mkdir_p($chunks_dir);
+            file_put_contents($chunks_dir . '/.htaccess', 'deny from all');
+            file_put_contents($chunks_dir . '/index.php', '<?php // Silence is golden');
+        }
+    }
+
+    public static function deactivate() {
+        // Clean up temp chunks
+        $upload_dir = wp_upload_dir();
+        $chunks_dir = $upload_dir['basedir'] . '/studiou-fpp-chunks';
+        if (is_dir($chunks_dir)) {
+            $files = glob($chunks_dir . '/*');
+            foreach ($files as $file) {
+                if (is_file($file) && basename($file) !== '.htaccess' && basename($file) !== 'index.php') {
+                    unlink($file);
+                }
+            }
+        }
+    }
+}
+
+// Activation/deactivation hooks
+register_activation_hook(__FILE__, array('Studiou_WC_Free_Photo_Product', 'activate'));
+register_deactivation_hook(__FILE__, array('Studiou_WC_Free_Photo_Product', 'deactivate'));
+
+function run_studiou_wc_free_photo_product() {
+    static $initialized = false;
+    if ($initialized) {
+        return;
+    }
+    $initialized = true;
+
+    load_plugin_textdomain(
+        'studiou-wc-free-photo-product',
+        false,
+        dirname(plugin_basename(__FILE__)) . '/languages/'
+    );
+
+    new Studiou_WC_Free_Photo_Product();
+}
+
+add_action('plugins_loaded', 'run_studiou_wc_free_photo_product');

+ 183 - 0
studiou-wc-free-photo-product/views/admin-summary.php

@@ -0,0 +1,183 @@
+<?php
+if (!defined('WPINC')) {
+    die;
+}
+
+/** @var array $records */
+/** @var int $total */
+/** @var int $total_pages */
+/** @var int $current_page */
+/** @var string $current_status */
+/** @var int $current_product */
+/** @var string $current_search */
+/** @var int $per_page */
+/** @var array $fpp_products */
+$base_url = admin_url('edit.php?post_type=product&page=studiou-fpp-summary');
+?>
+<div class="wrap studiou-fpp-admin-wrap">
+    <h1><?php esc_html_e('Free Photo Files', 'studiou-wc-free-photo-product'); ?></h1>
+
+    <div class="studiou-fpp-admin-notice-area"></div>
+
+    <!-- Filters -->
+    <div class="studiou-fpp-filters">
+        <form method="get" action="<?php echo esc_url(admin_url('edit.php')); ?>">
+            <input type="hidden" name="post_type" value="product" />
+            <input type="hidden" name="page" value="studiou-fpp-summary" />
+
+            <select name="status">
+                <option value=""><?php esc_html_e('All Statuses', 'studiou-wc-free-photo-product'); ?></option>
+                <option value="ready" <?php selected($current_status, 'ready'); ?>><?php esc_html_e('Ready', 'studiou-wc-free-photo-product'); ?></option>
+                <option value="downloaded" <?php selected($current_status, 'downloaded'); ?>><?php esc_html_e('Downloaded', 'studiou-wc-free-photo-product'); ?></option>
+                <option value="solved" <?php selected($current_status, 'solved'); ?>><?php esc_html_e('Solved', 'studiou-wc-free-photo-product'); ?></option>
+            </select>
+
+            <select name="product_id">
+                <option value=""><?php esc_html_e('All Products', 'studiou-wc-free-photo-product'); ?></option>
+                <?php foreach ($fpp_products as $pid => $pname) : ?>
+                    <option value="<?php echo esc_attr($pid); ?>" <?php selected($current_product, $pid); ?>><?php echo esc_html($pname); ?></option>
+                <?php endforeach; ?>
+            </select>
+
+            <input type="search" name="s" value="<?php echo esc_attr($current_search); ?>" placeholder="<?php esc_attr_e('Search file name...', 'studiou-wc-free-photo-product'); ?>" />
+
+            <button type="submit" class="button"><?php esc_html_e('Filter', 'studiou-wc-free-photo-product'); ?></button>
+        </form>
+    </div>
+
+    <!-- Bulk actions -->
+    <div class="studiou-fpp-bulk-actions">
+        <select id="studiou-fpp-bulk-action">
+            <option value=""><?php esc_html_e('Bulk Actions', 'studiou-wc-free-photo-product'); ?></option>
+            <option value="download_zip"><?php esc_html_e('Download ZIP', 'studiou-wc-free-photo-product'); ?></option>
+            <option value="status_ready"><?php esc_html_e('Set Status: Ready', 'studiou-wc-free-photo-product'); ?></option>
+            <option value="status_downloaded"><?php esc_html_e('Set Status: Downloaded', 'studiou-wc-free-photo-product'); ?></option>
+            <option value="status_solved"><?php esc_html_e('Set Status: Solved', 'studiou-wc-free-photo-product'); ?></option>
+            <option value="delete"><?php esc_html_e('Delete', 'studiou-wc-free-photo-product'); ?></option>
+        </select>
+        <button type="button" class="button" id="studiou-fpp-bulk-apply"><?php esc_html_e('Apply', 'studiou-wc-free-photo-product'); ?></button>
+        <span class="spinner" id="studiou-fpp-bulk-spinner"></span>
+
+        <span class="studiou-fpp-total-count">
+            <?php echo esc_html(sprintf(
+                _n('%d file', '%d files', $total, 'studiou-wc-free-photo-product'),
+                $total
+            )); ?>
+        </span>
+    </div>
+
+    <!-- Files table -->
+    <table class="wp-list-table widefat fixed striped studiou-fpp-files-table">
+        <thead>
+            <tr>
+                <td class="manage-column column-cb check-column">
+                    <input type="checkbox" id="studiou-fpp-select-all" />
+                </td>
+                <th class="column-thumbnail"><?php esc_html_e('Preview', 'studiou-wc-free-photo-product'); ?></th>
+                <th class="column-filename"><?php esc_html_e('File Name', 'studiou-wc-free-photo-product'); ?></th>
+                <th class="column-product"><?php esc_html_e('Product', 'studiou-wc-free-photo-product'); ?></th>
+                <th class="column-order"><?php esc_html_e('Order', 'studiou-wc-free-photo-product'); ?></th>
+                <th class="column-customer"><?php esc_html_e('Customer', 'studiou-wc-free-photo-product'); ?></th>
+                <th class="column-status"><?php esc_html_e('Status', 'studiou-wc-free-photo-product'); ?></th>
+                <th class="column-date"><?php esc_html_e('Date', 'studiou-wc-free-photo-product'); ?></th>
+                <th class="column-actions"><?php esc_html_e('Actions', 'studiou-wc-free-photo-product'); ?></th>
+            </tr>
+        </thead>
+        <tbody>
+            <?php if (empty($records)) : ?>
+                <tr>
+                    <td colspan="9"><?php esc_html_e('No files found.', 'studiou-wc-free-photo-product'); ?></td>
+                </tr>
+            <?php else : ?>
+                <?php foreach ($records as $record) :
+                    $attachment_url = wp_get_attachment_url($record->attachment_id);
+                    $thumb = wp_get_attachment_image_src($record->attachment_id, 'thumbnail');
+                    $thumb_url = $thumb ? $thumb[0] : wp_mime_type_icon($record->attachment_id);
+                    $product_title = get_the_title($record->product_id);
+
+                    $customer_name = '';
+                    if ($record->customer_id > 0) {
+                        $user = get_userdata($record->customer_id);
+                        $customer_name = $user ? $user->display_name : '#' . $record->customer_id;
+                    } else {
+                        $customer_name = __('Guest', 'studiou-wc-free-photo-product');
+                    }
+
+                    $order_link = '';
+                    if ($record->order_id > 0) {
+                        $order_url = Studiou_WC_FPP_Admin::get_order_edit_url($record->order_id);
+                        $order_link = '<a href="' . esc_url($order_url) . '">#' . esc_html($record->order_id) . '</a>';
+                    } else {
+                        $order_link = '<span class="studiou-fpp-no-order">—</span>';
+                    }
+                    ?>
+                    <tr data-file-id="<?php echo esc_attr($record->id); ?>">
+                        <th scope="row" class="check-column">
+                            <input type="checkbox" class="studiou-fpp-file-cb" value="<?php echo esc_attr($record->id); ?>" />
+                        </th>
+                        <td class="column-thumbnail">
+                            <img src="<?php echo esc_url($thumb_url); ?>" alt="" class="studiou-fpp-thumb" />
+                        </td>
+                        <td class="column-filename">
+                            <?php if ($attachment_url) : ?>
+                                <a href="<?php echo esc_url($attachment_url); ?>" target="_blank"><?php echo esc_html($record->file_name); ?></a>
+                            <?php else : ?>
+                                <?php echo esc_html($record->file_name); ?>
+                            <?php endif; ?>
+                        </td>
+                        <td class="column-product">
+                            <a href="<?php echo esc_url(get_edit_post_link($record->product_id)); ?>"><?php echo esc_html($product_title); ?></a>
+                        </td>
+                        <td class="column-order">
+                            <?php echo $order_link; ?>
+                        </td>
+                        <td class="column-customer">
+                            <?php echo esc_html($customer_name); ?>
+                        </td>
+                        <td class="column-status">
+                            <select class="studiou-fpp-status-select" data-file-id="<?php echo esc_attr($record->id); ?>">
+                                <option value="ready" <?php selected($record->status, 'ready'); ?>><?php esc_html_e('Ready', 'studiou-wc-free-photo-product'); ?></option>
+                                <option value="downloaded" <?php selected($record->status, 'downloaded'); ?>><?php esc_html_e('Downloaded', 'studiou-wc-free-photo-product'); ?></option>
+                                <option value="solved" <?php selected($record->status, 'solved'); ?>><?php esc_html_e('Solved', 'studiou-wc-free-photo-product'); ?></option>
+                            </select>
+                        </td>
+                        <td class="column-date">
+                            <?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($record->created_at))); ?>
+                        </td>
+                        <td class="column-actions">
+                            <?php if ($attachment_url) : ?>
+                                <a href="<?php echo esc_url($attachment_url); ?>" class="button button-small" download title="<?php esc_attr_e('Download', 'studiou-wc-free-photo-product'); ?>">
+                                    <span class="dashicons dashicons-download" style="line-height:1.4;"></span>
+                                </a>
+                            <?php endif; ?>
+                            <button type="button" class="button button-small studiou-fpp-delete-single" data-file-id="<?php echo esc_attr($record->id); ?>" title="<?php esc_attr_e('Delete', 'studiou-wc-free-photo-product'); ?>">
+                                <span class="dashicons dashicons-trash" style="line-height:1.4;color:#b32d2e;"></span>
+                            </button>
+                        </td>
+                    </tr>
+                <?php endforeach; ?>
+            <?php endif; ?>
+        </tbody>
+    </table>
+
+    <!-- Pagination -->
+    <?php if ($total_pages > 1) : ?>
+        <div class="tablenav bottom">
+            <div class="tablenav-pages">
+                <?php
+                $pagination = paginate_links(array(
+                    'base'      => add_query_arg('paged', '%#%'),
+                    'format'    => '',
+                    'current'   => $current_page,
+                    'total'     => $total_pages,
+                    'prev_text' => '&laquo;',
+                    'next_text' => '&raquo;',
+                ));
+                if ($pagination) {
+                    echo $pagination;
+                }
+                ?>
+            </div>
+        </div>
+    <?php endif; ?>
+</div>

+ 136 - 0
studiou-wc-free-photo-product/views/frontend-product.php

@@ -0,0 +1,136 @@
+<?php
+if (!defined('WPINC')) {
+    die;
+}
+
+/** @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-header">
+        <?php if ($product->get_image_id()) : ?>
+            <div class="studiou-fpp-product-image">
+                <?php echo $product->get_image('woocommerce_thumbnail'); ?>
+            </div>
+        <?php endif; ?>
+        <div class="studiou-fpp-product-info">
+            <h2 class="studiou-fpp-product-title"><?php echo esc_html($product->get_name()); ?></h2>
+            <?php if ($product->get_short_description()) : ?>
+                <div class="studiou-fpp-product-description">
+                    <?php echo wp_kses_post($product->get_short_description()); ?>
+                </div>
+            <?php endif; ?>
+        </div>
+    </div>
+
+    <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>
+    </div>
+
+</div>