CLAUDE.md 15 KB

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.5.14. 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 and preview on the WooCommerce product detail page.
  2. Product configuration area: Media Category for uploaded files, Maximum raw file size. Shortcode to embed product on any page.
  3. Uploaded raw files stored in WP media library with a specific media category per product. Chunked upload to bypass PHP upload size limits. Media files linked with product.
  4. Admin summary page: list of uploaded files, download as ZIP (bulk), order number, delete, change state (downloaded, ready, solved).
  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) — 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 + 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

  • 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
  • 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. 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.

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. 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.
  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. 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).
    • 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_ids 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 (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.
  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

  • 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
  • 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. A version bump also triggers maybe_upgrade_db()dbDelta on next load, which is idempotent.