Sfoglia il codice sorgente

1.5.0 phase 5: docs + i18n updates

- CLAUDE.md: rewrite Architecture, Key Classes, Frontend Flow sections
  for the new multi-upload / card-stack model. Document batch_token
  cookie, pvtfw suppression, new AJAX endpoints, automatic dbDelta
  upgrade on version mismatch.
- readme.md: prepend "What's new in 1.5.0" summary, rewrite Product
  Detail Page section to describe the card stack + order overview,
  document Max Uploads product setting, new AJAX endpoints, and
  batch_token column on the DB schema.
- .pot / cs_CZ.po: bump version to 1.5.0 and add translations for
  all new user-facing strings (upload card UI, order overview,
  error messages).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dalibor Votruba 2 mesi fa
parent
commit
e5b3fd4a6a

+ 48 - 35
studiou-wc-free-photo-product/CLAUDE.md

@@ -2,7 +2,7 @@
 
 ## Project Overview
 
-WordPress/WooCommerce plugin called **studiou-wc-free-photo-product** (QDR - Studiou WC Free Photo Product) v1.3.6.
+WordPress/WooCommerce plugin called **studiou-wc-free-photo-product** (QDR - Studiou WC Free Photo Product) v1.5.0.
 Allows publishing special variable products where customers upload custom raw images to be printed for a price.
 
 ## Initial Requirements
@@ -12,15 +12,16 @@ Allows publishing special variable products where customers upload custom raw im
 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).
+6. **1.5.0 change:** multi-upload — customers upload *many* photos in one visit, product page shows a vertical stack of cards (one per upload) instead of pvtfw's variant table. Each card has its own variant selector + qty + live price + Add-to-Cart button. Order-overview panel below lists this product's cart lines with per-line remove and subtotal. First uploaded file becomes the product hero image.
 
 ## Tech Stack
 
 - PHP 8.2+, WordPress 6.9.4+, WooCommerce 10.0+
-- Hard plugin dependency: **Product Variant Table for WooCommerce** (`pvtfw`, folder slug `product-variant-table-for-woocommerce`) — renders variants as a custom `<table class="variant">` with per-row qty inputs and its own `pvtfw_woocommerce_ajax_add_to_cart` handler. Declared via the `Requires Plugins:` header and enforced at runtime by `Studiou_WC_Free_Photo_Product::is_pvtfw_active()`.
+- Hard plugin dependency: **Product Variant Table for WooCommerce** (`pvtfw`, folder slug `product-variant-table-for-woocommerce`) — required for non-FPP variable products across the shop. **Suppressed on FPP products** since 1.5.0 — our card stack is the sole Add-to-Cart surface there. Declared via the `Requires Plugins:` header and enforced at runtime by `Studiou_WC_Free_Photo_Product::is_pvtfw_active()`.
 - jQuery for frontend/admin JS
-- WordPress AJAX (admin-ajax.php) for chunked upload
-- WC session storage for passing file data between upload and add-to-cart (theme-agnostic)
-- Custom DB table via dbDelta for file tracking
+- WordPress AJAX (admin-ajax.php) for chunked upload + per-card cart ops
+- First-party cookie `studiou_fpp_batch` as batch identifier — survives the classic-session <-> Store-API Cart-Token split
+- Custom DB table via dbDelta for file tracking (`wp_studiou_fpp_files`, with `batch_token` column indexed)
 - Custom taxonomy `studiou_media_category` on `attachment` post type
 
 ## Architecture
@@ -31,37 +32,49 @@ Allows publishing special variable products where customers upload custom raw im
 - JS namespace: `studiouWcfppFront` (frontend), `studiouWcfpp` (admin)
 - Nonces: `studiou-wcfpp-front-nonce` (frontend), `studiou-wcfpp-nonce` (admin)
 - Text domain: `studiou-wc-free-photo-product`
+- Schema upgrade is automatic: `maybe_upgrade_db()` on plugin load compares `studiou_wcfpp_db_version` option against `STUDIOU_WCFPP_VERSION` and runs `dbDelta` on mismatch.
 
 ## 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. On the final chunk also writes a first-party HttpOnly cookie pair (`studiou_fpp_att_id` / `studiou_fpp_rec_id`) in addition to WC session — this is the transport that survives the split between classic `wp_woocommerce_session_*` cookies and the WC Store API's Cart-Token (pvtfw 1.9.3+ / WC 10.7.0+ route Add to Cart through Store API, which sees a different empty session).
-- `Studiou_WC_FPP_Single_Product` - Upload area on product page (`woocommerce_single_product_summary` priority 35), WC session storage, server-side validation (`woocommerce_add_to_cart_validation` priority 1) + safety net (`woocommerce_add_to_cart` removes items without photo). Exposes the shared `resolve_uploaded_file(Studiou_WC_FPP_DB $db)` helper that reads WC session first, then falls back to the upload cookies, and verifies the record is unattached to an order.
-- `Studiou_WC_FPP_Shortcode` - `[studiou_free_photo_product id="X"]` renders product with WC native variation form
-- `Studiou_WC_FPP_Cart` - Reads file data from WC session via `woocommerce_add_cart_item_data`, replaces cart/order item thumbnails (classic cart via `woocommerce_cart_item_thumbnail`, block cart via `woocommerce_product_get_image_id`/`woocommerce_product_variation_get_image_id`), order item meta, order linking
-- `Studiou_WC_FPP_Admin` - Summary page with filters (status, product, order number, file-name search) and pagination, sortable columns (Order / Status / Date — default Date DESC, whitelist enforced in both the view and `Studiou_WC_FPP_DB::get_file_records`), status management, ZIP download, CSV export (bulk "Download as CSV" — writes a UTF-8-BOM CSV of the selected rows into `studiou-fpp-chunks/`, returns a nonce-protected download URL served by `handle_csv_download` and deleted after streaming), file download with auto-status-change, bulk actions with confirmation dialogs
-- `Studiou_WC_FPP_Pricing` - Per-variation quantity discount tiers. Admin editor on `woocommerce_variation_options_pricing`, save via `woocommerce_save_product_variation`, runtime discount via `woocommerce_before_calculate_totals` (priority 20) using `set_price()` on the cart item's product instance. Tier meta stored as a PHP array in `_studiou_fpp_qty_tiers` on each `product_variation` post. Feature is gated to FPP-enabled parent products. Frontend: pre-selection notice on `woocommerce_single_product_summary@25`, inline tier-data JSON emitted via `wp_footer` as `window.studiouFppTierData`, consumed by `initVariantTableTiers()` in `assets/js/frontend.js` to wire up per-row qty→price updates on the pvtfw variant table. Native WC `variations_form` handling is kept as a fallback path.
-
-## Frontend Flow
-
-Product page is rendered by the **Product Variant Table for WooCommerce** (`pvtfw`) plugin, which is a hard dependency. Each variation appears as a `<tr>` in `<table class="variant">` with its own `input.qty`, `.woocommerce-Price-amount`, and `.pvtfw_variant_table_cart_btn[data-variant="{variation_id}"]`. There is no WC native `form.variations_form` on the page.
-
-1. Upload area rendered below variation table via `woocommerce_single_product_summary` (priority 35)
-2. On page load, existing session state restored (preview, gallery image)
-3. Customer uploads photo via chunked AJAX
-4. On upload complete, JS stores file ref in WC session
-5. Product gallery image replaced with uploaded photo preview; restored on remove
-6. Customer selects variant and clicks any Add to Cart button (buttons look normal — no client-side disabling)
-7. `woocommerce_add_to_cart_validation` (priority 1) checks WC session — blocks with error notice if no photo
-8. `woocommerce_add_to_cart` safety net — removes item if it was added without photo data
-9. `woocommerce_add_cart_item_data` reads file data from WC session (session NOT cleared — same photo reusable for multiple variants)
-10. Cart thumbnails: classic cart via `woocommerce_cart_item_thumbnail`, block cart via `woocommerce_product_get_image_id`/`woocommerce_product_variation_get_image_id`
-11. Order admin: thumbnail via `woocommerce_admin_order_item_thumbnail`, download link via `woocommerce_after_order_itemmeta` (auto-marks status as "downloaded")
-12. On checkout, file record linked to order via `woocommerce_checkout_order_processed` (classic), `woocommerce_store_api_checkout_order_processed` (block), `woocommerce_thankyou` (fallback)
-13. Session only cleared when user clicks Remove
-14. Admin photo files view: download marks "ready" -> "downloaded", bulk actions with confirmations, pagination preserves filters
-15. **Quantity discount UI**: the pricing class emits `window.studiouFppTierData` in `wp_footer`; `initVariantTableTiers()` finds each row by `data-variant`, appends a tier strip under the price cell, and binds `input.qty` / `.qty-count` clicks to recompute the displayed price. Server-side `woocommerce_before_calculate_totals` (priority 20) remains authoritative and calls `set_price()` on each matching cart item regardless of how it was added.
+- `Studiou_WC_FPP_DB` - Custom table CRUD, media category taxonomy. Batch-aware queries: `get_batch_files($token, $product_id)`, `count_batch_files($token, $product_id)`.
+- `Studiou_WC_FPP_Product` - WC product data tab, product meta (including `_studiou_fpp_max_uploads` — per-product cap on simultaneous uploads, 0 = unlimited).
+- `Studiou_WC_FPP_Upload` - Chunked upload AJAX, file assembly, attachment creation. Mints/reuses a 32-char `studiou_fpp_batch` cookie on first chunk-upload completion and persists it in the new file record's `batch_token` column. Enforces the per-product max-uploads cap before accepting chunk 0. Legacy `studiou_fpp_att_id`/`studiou_fpp_rec_id` cookies still written for one release as a read-fallback for the shortcode path.
+- `Studiou_WC_FPP_Single_Product` - On `wp` priority 5, detaches pvtfw's variant table (`remove_action()` on the hook/priority read from pvtfw's own `pvtfw_variant_table_place` option) and pvtfw's available-options button on FPP products. On `woocommerce_single_product_summary` priority 35 renders the upload dropzone + card stack + order-overview panel. Exposes `resolve_batch_files(Studiou_WC_FPP_DB $db, $product_id = 0)` (list, 1.5.0 path) and `resolve_uploaded_file()` (single, legacy shortcode path). Adds `studiou-fpp-product-page` body class. Server-side validation (`woocommerce_add_to_cart_validation` priority 1) accepts `$cart_item_data['studiou_fpp_file_record_id']` as the primary "photo present" signal, with the resolver as fallback.
+- `Studiou_WC_FPP_Shortcode` - `[studiou_free_photo_product id="X"]` renders product with WC native variation form (unchanged — still the single-file flow).
+- `Studiou_WC_FPP_Cart` - New AJAX endpoints `studiou_wcfpp_add_file_to_cart` (per-card Add-to-Cart, pre-stamps `file_record_id` on `$cart_item_data` and calls `WC()->cart->add_to_cart()`) and `studiou_wcfpp_remove_cart_line` (cart-only remove from overview panel). `add_cart_item_data()` prefers pre-stamped `file_record_id` from the new flow, falls back to the legacy visitor-level resolver. Replaces cart/order item thumbnails (classic cart via `woocommerce_cart_item_thumbnail`, block cart via `woocommerce_product_get_image_id`/`woocommerce_product_variation_get_image_id`), order item meta, order linking. `render_overview_html($product_id)` returns the panel HTML for in-place re-render after cart changes.
+- `Studiou_WC_FPP_Admin` - Summary page with filters (status, product, order number, file-name search) and pagination, sortable columns (Order / Status / Date — default Date DESC), status management, ZIP download, CSV export, file download with auto-status-change, bulk actions with confirmation dialogs.
+- `Studiou_WC_FPP_Pricing` - Per-variation quantity discount tiers. Admin editor on `woocommerce_variation_options_pricing`, save via `woocommerce_save_product_variation`, runtime discount via `woocommerce_before_calculate_totals` (priority 20) using `set_price()` on the cart item's product instance. Tier meta stored as a PHP array in `_studiou_fpp_qty_tiers` on each `product_variation` post. Feature is gated to FPP-enabled parent products. **Frontend (1.5.0+):** tier data is consumed directly by the upload-card JS — each card's `updateCardPrice()` reads the selected variant's tiers (embedded in `studiouFppCardData.variants`) and re-renders its price + `−X%` badge on variant/qty changes. `initVariantTableTiers()` (pvtfw) and `initQtyTiers()` (native WC form) were removed in 1.5.0.
+
+## Frontend Flow (1.5.0+)
+
+**FPP product page (`is_product()` + `is_fpp_product()`):**
+
+1. `maybe_suppress_pvtfw()` detaches pvtfw's `print_table` and `available_options_btn` hooks on `wp` priority 5.
+2. `render_upload_area()` on `woocommerce_single_product_summary` priority 35 passes variants + max uploads + existing batch files + hero preview URL to JS via `wp_localize_script('studiou-wcfpp-frontend', 'studiouFppCardData', ...)`.
+3. Browser renders:
+   - **Dropzone** (multi-file `<input type="file" multiple>`) — accepts drag-drop + click-to-browse.
+   - **Cards stack** — one card per upload, all built client-side by `buildCard()` from `studiouFppCardData`.
+   - **Order overview panel** — rendered server-side initially from `WC()->cart` filtered to the current product; re-rendered in-place after each cart change.
+4. Customer drops photos → `enqueueFiles()` creates a "uploading" card per file, queue processes sequentially via chunked AJAX.
+5. Server-side upload handler:
+   - `resolve_or_mint_batch_token()` — reads `studiou_fpp_batch` cookie or mints a new 32-char token and sets the cookie.
+   - On chunk 0 of a new file, counts existing batch files for this product; rejects if `_studiou_fpp_max_uploads` is reached.
+   - On final chunk: `assemble_chunks()` creates the attachment and the DB row with `batch_token` stamped.
+6. On upload complete, JS swaps the card to "ready" state (thumb + variant dropdown + qty + price + Add to Cart + × Remove Upload).
+7. If this is the first card in the stack, JS also swaps the product-gallery image to the upload preview via `applyHeroImage()`. Remove-first-upload reverts to the next card's image or restores the original via `restoreHeroImage()`.
+8. Customer picks variant + qty on a card → `updateCardPrice()` computes (base_price × qty) with the applicable tier applied; shows `−X%` badge when a tier is active.
+9. Click "Add to cart" → `studiou_wcfpp_add_file_to_cart` AJAX with `file_record_id + variation_id + quantity`. Server:
+   - Verifies nonce + record ownership (`batch_token` match).
+   - Pre-stamps `$cart_item_data['studiou_fpp_file_record_id']`.
+   - Calls `WC()->cart->add_to_cart($parent_id, $qty, $variation_id, $variation_attrs, $cart_item_data)`.
+   - Returns `overview_html` (freshly-rendered panel) for in-place replacement.
+10. `woocommerce_add_cart_item_data` (our filter) reads the pre-stamped `file_record_id`, looks up the record, stamps `studiou_fpp_attachment_id` / `_file_name` / `_thumb_url` on the cart item, and updates the record's `variation_id`. Each (upload × variant × qty) Add-to-Cart creates a distinct cart line (WC's `generate_cart_id()` hashes `$cart_item_data`, so different `file_record_id`s never collapse).
+11. `woocommerce_before_calculate_totals` priority 20 (`Pricing::apply_cart_discount`) re-applies tier discounts on every recalc — authoritative server-side pricing.
+12. × on an upload card → `studiou_wcfpp_remove_upload` (deletes the DB row + attachment, clears legacy cookies). × on an overview line → `studiou_wcfpp_remove_cart_line` (cart-only; upload stays intact).
+13. Cart/order thumbnails: classic cart via `woocommerce_cart_item_thumbnail`, block cart via `woocommerce_product_get_image_id`/`woocommerce_product_variation_get_image_id`.
+14. Order admin: thumbnail via `woocommerce_admin_order_item_thumbnail`, download link via `woocommerce_after_order_itemmeta` (auto-marks status as "downloaded").
+15. On checkout, file records linked to the order via `woocommerce_checkout_order_processed` (classic), `woocommerce_store_api_checkout_order_processed` (block), `woocommerce_thankyou` (fallback). Each cart line becomes its own order item with its own photo meta.
+16. **Shortcode path `[studiou_free_photo_product id="X"]`** keeps the single-file 1.3.x UX — one dropzone, one preview, one `attachment_id`, add-to-cart via WC's native `variations_form`. The pvtfw suppression doesn't touch shortcode-embedded products (they're not rendered via `single-product.php`).
 
 ## Coding Conventions
 
@@ -78,10 +91,10 @@ Product page is rendered by the **Product Variant Table for WooCommerce** (`pvtf
 
 - Generate .mo from .po: `php languages/generate-mo.php` or `wp i18n make-mo languages/`
 - Plugin creates DB table on activation via `register_activation_hook`
-- Activation hook requires explicit `require_once` for DB class (runs before `plugins_loaded`)
+- **Schema upgrades are automatic on load** (no manual migration needed): `maybe_upgrade_db()` calls `create_tables()` → `dbDelta` whenever `studiou_wcfpp_db_version` is older than `STUDIOU_WCFPP_VERSION`. The 1.5.0 upgrade adds the `batch_token` column + index to existing installs transparently.
 - 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. Also bump the version string in `languages/studiou-wc-free-photo-product.pot` and `languages/studiou-wc-free-photo-product-cs_CZ.po` header, and update the `v…` reference at the top of this CLAUDE.md.
+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. Also bump the version string in `languages/studiou-wc-free-photo-product.pot` and `languages/studiou-wc-free-photo-product-cs_CZ.po` header, and update the `v…` reference at the top of this CLAUDE.md. A version bump also triggers `maybe_upgrade_db()` → `dbDelta` on next load, which is idempotent.

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

@@ -3,7 +3,7 @@
 # This file is distributed under the GPL v2 or later.
 msgid ""
 msgstr ""
-"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.3.6\n"
+"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.5.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"
@@ -417,3 +417,62 @@ msgstr "ID varianty"
 
 msgid "File URL"
 msgstr "URL souboru"
+
+# --- 1.5.0 strings ---
+
+msgid "Max Uploads"
+msgstr "Maximální počet nahrání"
+
+msgid "Maximum number of photos a customer can upload for this product in a single visit. Leave empty or 0 for unlimited."
+msgstr "Maximální počet fotografií, které může zákazník pro tento produkt nahrát během jedné návštěvy. Nechte prázdné nebo 0 pro neomezený počet."
+
+msgid "Upload Your Photos"
+msgstr "Nahrajte své fotografie"
+
+msgid "Upload up to %2$d photos (max %1$d MB each). Accepted formats: JPEG, PNG, TIFF, BMP, PSD, RAW (CR2, NEF, ARW, DNG, ORF, RW2, RAF), WebP."
+msgstr "Nahrajte až %2$d fotografií (max. %1$d MB každá). Podporované formáty: JPEG, PNG, TIFF, BMP, PSD, RAW (CR2, NEF, ARW, DNG, ORF, RW2, RAF), WebP."
+
+msgid "Maximum file size: %d MB. Accepted formats: JPEG, PNG, TIFF, BMP, PSD, RAW (CR2, NEF, ARW, DNG, ORF, RW2, RAF), WebP."
+msgstr "Maximální velikost souboru: %d MB. Podporované formáty: JPEG, PNG, TIFF, BMP, PSD, RAW (CR2, NEF, ARW, DNG, ORF, RW2, RAF), WebP."
+
+msgid "Drag & drop photos here, or click to choose files"
+msgstr "Přetáhněte fotografie sem, nebo klikněte pro výběr"
+
+msgid "Your order"
+msgstr "Vaše objednávka"
+
+msgid "Qty %d"
+msgstr "Ks %d"
+
+msgid "Subtotal:"
+msgstr "Mezisoučet:"
+
+msgid "Go to checkout"
+msgstr "Přejít k pokladně"
+
+msgid "Maximum number of uploads reached (%d). Remove a photo before uploading another."
+msgstr "Byl dosažen maximální počet nahrání (%d). Před nahráním další fotografie nějakou odstraňte."
+
+msgid "Invalid request."
+msgstr "Neplatný požadavek."
+
+msgid "Invalid variant."
+msgstr "Neplatná varianta."
+
+msgid "Cart unavailable."
+msgstr "Košík není dostupný."
+
+msgid "Cart line not found."
+msgstr "Položka košíku nenalezena."
+
+msgid "Could not add to cart."
+msgstr "Nelze přidat do košíku."
+
+msgid "Add to cart"
+msgstr "Do košíku"
+
+msgid "Maximum number of uploads reached (%d)."
+msgstr "Byl dosažen maximální počet nahrání (%d)."
+
+msgid "out of stock"
+msgstr "není skladem"

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

@@ -2,7 +2,7 @@
 # This file is distributed under the GPL v2 or later.
 msgid ""
 msgstr ""
-"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.3.6\n"
+"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.5.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"
@@ -548,3 +548,85 @@ msgstr ""
 #: includes/class-studiou-wc-fpp-admin.php
 msgid "File URL"
 msgstr ""
+
+# --- 1.5.0 strings ---
+
+#: includes/class-studiou-wc-fpp-product.php
+msgid "Max Uploads"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-product.php
+msgid "Maximum number of photos a customer can upload for this product in a single visit. Leave empty or 0 for unlimited."
+msgstr ""
+
+#: views/single-product-upload.php
+msgid "Upload Your Photos"
+msgstr ""
+
+#: views/single-product-upload.php
+msgid "Upload up to %2$d photos (max %1$d MB each). Accepted formats: JPEG, PNG, TIFF, BMP, PSD, RAW (CR2, NEF, ARW, DNG, ORF, RW2, RAF), WebP."
+msgstr ""
+
+#: views/single-product-upload.php
+msgid "Maximum file size: %d MB. Accepted formats: JPEG, PNG, TIFF, BMP, PSD, RAW (CR2, NEF, ARW, DNG, ORF, RW2, RAF), WebP."
+msgstr ""
+
+#: views/single-product-upload.php
+msgid "Drag & drop photos here, or click to choose files"
+msgstr ""
+
+#: views/single-product-order-overview.php
+msgid "Your order"
+msgstr ""
+
+#: views/single-product-order-overview.php
+msgid "Qty %d"
+msgstr ""
+
+#: views/single-product-order-overview.php
+msgid "Remove"
+msgstr ""
+
+#: views/single-product-order-overview.php
+msgid "Subtotal:"
+msgstr ""
+
+#: views/single-product-order-overview.php
+msgid "Go to checkout"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-upload.php
+msgid "Maximum number of uploads reached (%d). Remove a photo before uploading another."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-cart.php
+msgid "Invalid request."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-cart.php
+msgid "Invalid variant."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-cart.php
+msgid "Cart unavailable."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-cart.php
+msgid "Cart line not found."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-cart.php
+msgid "Could not add to cart."
+msgstr ""
+
+#: studiou-wc-free-photo-product.php
+msgid "Add to cart"
+msgstr ""
+
+#: studiou-wc-free-photo-product.php
+msgid "Maximum number of uploads reached (%d)."
+msgstr ""
+
+#: studiou-wc-free-photo-product.php
+msgid "out of stock"
+msgstr ""

+ 28 - 10
studiou-wc-free-photo-product/readme.md

@@ -1,6 +1,10 @@
 # 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 using WooCommerce's native variation selector and upload their photo file, which gets stored in the WordPress media library under a configurable media category.
+WordPress (6.9.4+) / WooCommerce (10.6.2+) plugin that allows publishing special variable products with custom raw image upload for photo printing. Customers upload one or more photos on the product page and compose an order where each uploaded photo can be printed in a different variant at its own quantity. Uploaded files are stored in the WordPress media library under a configurable media category.
+
+## What's new in 1.5.0
+
+Multi-upload overhaul. The product-detail page now shows a **vertical stack of cards** — one per uploaded photo — in place of the pvtfw variant table on FPP products. Each card carries its own variant selector, quantity stepper, live tier-discounted price (with `−X%` badge), and its own Add-to-Cart button. An **order-overview panel** below the cards lists the lines composed for this product, with per-line remove and subtotal. The first uploaded image becomes the product hero thumbnail on that page. A per-product **Max Uploads** setting caps how many photos a customer can queue in one visit (default 0 = unlimited).
 
 ## Requirements
 
@@ -8,7 +12,7 @@ WordPress (6.9.4+) / WooCommerce (10.6.2+) plugin that allows publishing special
 - WooCommerce 10.0+
 - PHP 8.2+
 - PHP ZipArchive extension (for admin ZIP download)
-- **Product Variant Table for WooCommerce** plugin (folder slug: `product-variant-table-for-woocommerce`) — used to render variants as a table with per-row quantity inputs. Declared via the plugin header `Requires Plugins:` field and enforced at runtime by an admin notice if the plugin is missing or deactivated.
+- **Product Variant Table for WooCommerce** plugin (folder slug: `product-variant-table-for-woocommerce`) — still required as a dependency for non-FPP variable products across the shop. On FPP products the plugin **suppresses pvtfw's rendering** (via `remove_action()` on the hook/priority it reads from pvtfw's own `pvtfw_variant_table_place` option) and replaces it with its own upload-card stack. Declared via the plugin header `Requires Plugins:` field and enforced at runtime by an admin notice if the plugin is missing or deactivated.
 
 ## Installation
 
@@ -27,19 +31,29 @@ WordPress (6.9.4+) / WooCommerce (10.6.2+) plugin that allows publishing special
 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. Optionally set **Min Image Resolution (px)** and **Max Image Resolution (px)** - width and height are interchangeable (portrait/landscape auto-detected). Leave empty to skip validation. RAW files that cannot be measured are skipped.
+7.5. Optionally set **Max Uploads** — cap on simultaneous uploads per customer visit for this product. Leave empty or 0 for unlimited.
 8. Copy the displayed **Shortcode** for use on custom pages
 
-### Product Detail Page
+### Product Detail Page (1.5.0+)
+
+On FPP products, the plugin **suppresses pvtfw's variant table** (via `remove_action()` on pvtfw's configured place hook) and renders its own UI in its stead:
+
+1. An upload **dropzone** (multi-file) at the top of the area.
+2. A **vertical stack of cards**, one per uploaded photo. Each card contains: thumbnail + filename + remove-upload (×) button + variant selector + `−/+` quantity stepper + live tier-discounted price with `−X%` discount badge + Add-to-Cart button.
+3. An **order-overview panel** at the bottom scoped to this product's FPP cart lines: thumbnail + variant label + quantity + line total + discount badge + per-line remove link + subtotal + "Go to checkout" button.
+
+The customer workflow is:
 
-When a variable product has Free Photo enabled, the upload area automatically appears on the WooCommerce single product page **below** the product summary (below the variation table and Add to Cart buttons). The customer workflow is:
+1. Drop or pick one or more photos — they upload sequentially in the same card stack (progress bar overlaid on the thumbnail while uploading).
+2. The first uploaded photo becomes the product's hero gallery image; it's swapped back if all photos are removed.
+3. For each card: pick a variant and a quantity — the price updates live, a `−X%` badge appears when a quantity-discount tier is active.
+4. Click **Add to cart** on any card — the backing AJAX pre-stamps the `file_record_id` on `$cart_item_data` and calls `WC()->cart->add_to_cart()`. A new cart line (and eventually a new order item) is created for each (photo × variant × qty) combination. Adding the same combination twice increments the existing line's quantity.
+5. The order-overview panel refreshes in place with the new line.
+6. × on a card removes the upload (deletes the media file and DB record). × on an overview line removes the cart line only — the upload stays in the card stack so it can be re-added with different options.
 
-1. Upload a photo file via drag-and-drop or file browser (below the variation table)
-2. The product gallery image updates to show the uploaded photo preview
-3. Select a variant from the product variation table and click its **Add to Cart** button
-4. If no photo is uploaded, the server blocks the add-to-cart and shows an error notice
-5. The uploaded photo stays — the same photo can be used for multiple variants without re-uploading
+**Identity transport (1.5.0).** The plugin uses a single first-party HttpOnly cookie `studiou_fpp_batch` holding a 32-character random token. The token is minted the first time a photo finishes uploading and is reused by every subsequent upload in the same browser. Each `wp_studiou_fpp_files` row carries this token in a new `batch_token` column, so the server can enumerate exactly the photos this visitor has uploaded regardless of which WooCommerce session handler answers the Add-to-Cart request (classic cookie session vs. Store API Cart-Token — the cookie travels with *every* same-origin request). The legacy single-file `studiou_fpp_att_id` / `studiou_fpp_rec_id` cookies are still written by the upload handler as a read-fallback for the shortcode path; they'll be removed in 1.5.1.
 
-The uploaded file reference is stored in two parallel locations — the WooCommerce session **and** a pair of first-party HttpOnly cookies (`studiou_fpp_att_id` / `studiou_fpp_rec_id`) written server-side on the same request that assembles the file. The cookie pair is required because Product Variant Table for WooCommerce 1.9.3+ and WooCommerce 10.7.0+ route Add to Cart through the WC **Store API**, which identifies its session via the `Cart-Token` request header rather than the classic `wp_woocommerce_session_*` cookie — and that path sees a different, empty session than the one the upload wrote to. The cookies travel with *every* same-origin request (classic admin-ajax **and** Store API fetch calls), so whichever endpoint PVT / WC decide to use, the upload reference is reachable. Both the session and the cookies persist across add-to-cart actions, allowing the same photo to be used for multiple variants without re-uploading; both are cleared when the user explicitly removes the uploaded photo.
+**Shortcode path.** `[studiou_free_photo_product id="X"]` keeps the 1.3.x single-file UX — one dropzone, one preview, add-to-cart via WC's native `variations_form` — because it embeds the product on arbitrary pages where the pvtfw suppression doesn't apply.
 
 ### Quantity Discount Tiers (per variation)
 
@@ -185,6 +199,7 @@ The plugin creates one custom table on activation:
 | 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 |
+| batch_token | varchar(32) | 1.5.0: per-visit opaque token identifying the upload batch |
 | file_name | varchar(255) | Original uploaded file name |
 | status | varchar(20) | ready / downloaded / solved |
 | created_at | datetime | Upload timestamp |
@@ -235,6 +250,8 @@ studiou-wc-free-photo-product/
 | `studiou_wcfpp_delete_files` | Admin | Delete files (single or bulk) |
 | `studiou_wcfpp_download_zip` | Admin | Generate ZIP of selected files |
 | `studiou_wcfpp_download_csv` | Admin | Export selected file rows to a UTF-8 CSV (Excel-friendly) |
+| `studiou_wcfpp_add_file_to_cart` | Both | 1.5.0: per-card Add-to-Cart — adds one (file × variant × qty) as a new cart line |
+| `studiou_wcfpp_remove_cart_line` | Both | 1.5.0: remove one cart line from the order-overview panel (cart-only, upload stays) |
 
 ### WooCommerce Hooks Used
 
@@ -268,6 +285,7 @@ studiou-wc-free-photo-product/
 | `_studiou_fpp_enabled` | "yes" / "no" |
 | `_studiou_fpp_media_category` | Term ID of media category |
 | `_studiou_fpp_max_file_size` | Max upload size in MB |
+| `_studiou_fpp_max_uploads` | 1.5.0: max simultaneous uploads per visit (0 = unlimited) |
 | `_studiou_fpp_min_width` | Min image width in px (0 = no limit) |
 | `_studiou_fpp_min_height` | Min image height in px (0 = no limit) |
 | `_studiou_fpp_max_width` | Max image width in px (0 = no limit) |