WordPress/WooCommerce plugin called studiou-wc-free-photo-product (QDR - Studiou WC Free Photo Product) v1.5.14. Allows publishing special variable products where customers upload custom raw images to be printed for a price.
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().studiou_fpp_batch as batch identifier — survives the classic-session <-> Store-API Cart-Token splitwp_studiou_fpp_files, with batch_token column indexed)studiou_media_category on attachment post typeStudiou_WC_Free_Photo_Product in root plugin fileSTUDIOU_WCFPP_studiou-fpp-studiouWcfppFront (frontend), studiouWcfpp (admin)studiou-wcfpp-front-nonce (frontend), studiou-wcfpp-nonce (admin)studiou-wc-free-photo-productmaybe_upgrade_db() on plugin load compares studiou_wcfpp_db_version option against STUDIOU_WCFPP_VERSION and runs dbDelta on mismatch.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. The batch cookie is the sole transport since 1.5.3 — legacy WC-session keys and the short-lived studiou_fpp_att_id/studiou_fpp_rec_id cookie pair have been removed.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. pvtfw's singletons are reached via PVTFW_PRINT_TABLE::instance() / PVTFW_AVAILABE_BTN::instance() (the $pvtfw_print_table / $pvtfw_available_btn globals published by pvtfw end up as method-locals because pvtfw's require_once lives inside a method — the static singletons return the same real instances that pvtfw used to register the hooks). 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 (Store API) via woocommerce_get_cart_item_from_session which clones the cart line's product and calls set_image_id() on the clone — this is cart-item-scoped so multiple lines sharing a variation_id still render their own uploaded photo (the previous woocommerce_product_get_image_id override fired per-product and mismatched thumbnails when several lines shared a variation, fixed in 1.5.4). 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.FPP product page (is_product() + is_fpp_product()):
maybe_suppress_pvtfw() detaches pvtfw's print_table and available_options_btn hooks on wp priority 5.
1a. (1.5.12+) maybe_suppress_pvtfw() also detaches woocommerce_template_single_price at priority 10 so WC's price-range span (e.g. "10,00 Kč – 40,00 Kč") is hidden on FPP products — the per-variant table below replaces it.
1b. (1.5.12+) render_variant_price_table() on woocommerce_single_product_summary priority 34 renders a read-only server-side table (views/single-product-price-table.php) above the dropzone: one row per variant with unit price (via wc_price()) and tiered quantity-discount badges. Pure overview — no radio/qty/add-to-cart.
1c. (1.5.12+) override_wc_strings() on gettext_woocommerce reroutes selected WC core strings (currently "Estimated total") through our own textdomain so the translation lives in our .po. This catches PHP-rendered strings only. (1.5.13+) The block cart is React-rendered and reads strings from wp.i18n instead, so an inline script on the wp-i18n handle calls wp.i18n.setLocaleData({ 'Estimated total': […], 'Cart totals': […] }, 'woocommerce') after wp-i18n loads — overriding the bundled "Odhadovaný součet" in the live DOM.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', ...).<input type="file" multiple>) — accepts drag-drop + click-to-browse. Click-to-browse opens the native file picker via $fileInput[0].click() (NOT $fileInput.trigger('click')), because the <input> lives inside the dropzone element and a bubbling jQuery trigger re-entered the dropzone's own click handler, causing Maximum call stack size exceeded on mobile where the browser synthesizes an extra click when the picker closes (fixed in 1.5.11).buildCard() from studiouFppCardData.WC()->cart filtered to the current product; re-rendered in-place after each cart change.enqueueFiles() creates a "uploading" card per file, queue processes sequentially via chunked AJAX.resolve_or_mint_batch_token() — reads studiou_fpp_batch cookie or mints a new 32-char token and sets the cookie._studiou_fpp_max_uploads is reached.assemble_chunks() creates the attachment and the DB row with batch_token stamped.applyHeroImage(). Remove-first-upload reverts to the next card's image or restores the original via restoreHeroImage().updateCardPrice() computes (base_price × qty) with the applicable tier applied; shows −X% badge when a tier is active.studiou_wcfpp_add_file_to_cart AJAX with file_record_id + variation_id + quantity. Server:
batch_token match).$cart_item_data['studiou_fpp_file_record_id'].WC()->cart->add_to_cart($parent_id, $qty, $variation_id, $variation_attrs, $cart_item_data).overview_html (freshly-rendered panel) for in-place replacement.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_ids never collapse).woocommerce_before_calculate_totals priority 20 (Pricing::apply_cart_discount) re-applies tier discounts on every recalc — authoritative server-side pricing.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).woocommerce_cart_item_thumbnail; block cart (Store API) via woocommerce_get_cart_item_from_session — we clone the cart line's product and set_image_id() it to the uploaded attachment, so each line renders its own photo even when several lines share a variation.
13a. Lightbox (1.5.4+): opens .studiou-fpp-lightbox with the attachment's woocommerce_single size preview. Primary opener (1.5.7+): an explicit "Enlarge" button — .studiou-fpp-card-enlarge in each card's price-row (sits left of Add-to-Cart, away from the top-right × remove button) and .studiou-fpp-overview-enlarge on each overview line. Buttons are wired via direct element.onclick = onEnlargeClick at creation/render time so theme-level click interceptors can't swallow them. Secondary opener: clicking the thumbnail itself (element-level listener on .studiou-fpp-card-thumb via bindThumbClick, plus a document-level capture-phase listener). Closed via × button, backdrop click, or Escape. Disabled on cards still uploading. If the overlay DOM somehow fails to initialize, openLightbox() falls back to window.open(url, '_blank') so the button is never a no-op.woocommerce_admin_order_item_thumbnail, download link via woocommerce_after_order_itemmeta (auto-marks status as "downloaded").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.[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).studiou-wc-product-cat-manageif (!defined('WPINC')) { die; }wp_send_json_success / wp_send_json_error for all AJAX responses__() or esc_html_e() with text domainabsint(), sanitize_text_field(), sanitize_file_name()esc_html(), esc_attr(), esc_url(), wp_kses_post()php languages/generate-mo.php or wp i18n make-mo languages/register_activation_hookmaybe_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.wp-content/uploads/studiou-fpp-chunks/ (cleaned on deactivation)wp-content/uploads/YYYY/MM/free-photo/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.