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