d36bb954d1
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
248 lines
7.5 KiB
JavaScript
248 lines
7.5 KiB
JavaScript
/**
|
||
* WP Data Optimizer — REST API JavaScript SDK
|
||
*
|
||
* Lightweight client for /wp-json/wpdo/v1/ endpoints.
|
||
* No dependencies required.
|
||
*
|
||
* Usage:
|
||
* const wpdo = new WpdoClient();
|
||
*
|
||
* // Fetch listings (Zone A)
|
||
* const { items, total } = await wpdo.getListings({ hp_price_min: 100, per_page: 20 });
|
||
*
|
||
* // Single listing (Zone A + Zone C)
|
||
* const listing = await wpdo.getListing(123);
|
||
*
|
||
* // View count (Zone B)
|
||
* const { view_count } = await wpdo.getStats(123);
|
||
*
|
||
* // Increment view count (Zone B, requires WP REST nonce)
|
||
* const { view_count: updated } = await wpdo.incrementView(123);
|
||
*
|
||
* // Iterate all pages
|
||
* for await (const page of wpdo.paginateListings({ hp_featured: 1 })) {
|
||
* console.log(page.items);
|
||
* }
|
||
*
|
||
* @package WP_Data_Optimizer
|
||
*/
|
||
|
||
/* global wpdo_sdk_config */
|
||
|
||
( function ( global ) {
|
||
'use strict';
|
||
|
||
/**
|
||
* WpdoClient — REST API wrapper for WP Data Optimizer.
|
||
*
|
||
* @param {object} [options]
|
||
* @param {string} [options.baseUrl] REST base URL (default: auto-detected from wpdo_sdk_config or /wp-json)
|
||
* @param {string} [options.nonce] WP REST nonce for authenticated requests
|
||
* @param {string} [options.postType] Default post type (default: 'hp_listing')
|
||
*/
|
||
function WpdoClient( options ) {
|
||
options = options || {};
|
||
|
||
var config = ( typeof wpdo_sdk_config !== 'undefined' ) ? wpdo_sdk_config : {};
|
||
|
||
this._base = options.baseUrl || config.rest_url || '/wp-json/wpdo/v1';
|
||
this._nonce = options.nonce || config.nonce || '';
|
||
this._postType = options.postType || config.post_type || 'hp_listing';
|
||
}
|
||
|
||
// ── Core fetch ────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Internal GET fetch helper.
|
||
*
|
||
* @param {string} path Relative path (e.g. '/listings')
|
||
* @param {object} params Query parameters
|
||
* @return {Promise<{data: *, headers: Headers, status: number}>}
|
||
*/
|
||
WpdoClient.prototype._fetch = function ( path, params ) {
|
||
var url = this._base + path;
|
||
|
||
if ( params && Object.keys( params ).length ) {
|
||
var qs = Object.keys( params )
|
||
.filter(
|
||
function ( k ) {
|
||
return params[ k ] !== null && params[ k ] !== undefined && params[ k ] !== ''; }
|
||
)
|
||
.map(
|
||
function ( k ) {
|
||
return encodeURIComponent( k ) + '=' + encodeURIComponent( params[ k ] ); }
|
||
)
|
||
.join( '&' );
|
||
if ( qs ) {
|
||
url += '?' + qs;
|
||
}
|
||
}
|
||
|
||
var headers = { 'Content-Type': 'application/json' };
|
||
if ( this._nonce ) {
|
||
headers[ 'X-WP-Nonce' ] = this._nonce;
|
||
}
|
||
|
||
return fetch( url, { headers: headers } ).then(
|
||
function ( res ) {
|
||
return res.json().then(
|
||
function ( data ) {
|
||
return { data: data, headers: res.headers, status: res.status };
|
||
}
|
||
);
|
||
}
|
||
);
|
||
};
|
||
|
||
/**
|
||
* Internal POST fetch helper.
|
||
*
|
||
* @param {string} path Relative path
|
||
* @param {object} body JSON body (optional)
|
||
* @return {Promise<{data: *, status: number}>}
|
||
*/
|
||
WpdoClient.prototype._post = function ( path, body ) {
|
||
var headers = { 'Content-Type': 'application/json' };
|
||
if ( this._nonce ) {
|
||
headers[ 'X-WP-Nonce' ] = this._nonce;
|
||
}
|
||
|
||
return fetch(
|
||
this._base + path,
|
||
{
|
||
method: 'POST',
|
||
headers: headers,
|
||
body: body ? JSON.stringify( body ) : null,
|
||
}
|
||
).then(
|
||
function ( res ) {
|
||
return res.json().then(
|
||
function ( data ) {
|
||
return { data: data, status: res.status };
|
||
}
|
||
);
|
||
}
|
||
);
|
||
};
|
||
|
||
// ── Public API ────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Fetch a page of listings from Zone A.
|
||
*
|
||
* @param {object} [params]
|
||
* @param {string} [params.post_type] Default: configured post type
|
||
* @param {number} [params.per_page] 1–100, default 20
|
||
* @param {number} [params.page] Default 1
|
||
* @param {string} [params.orderby] Column name, default 'post_id'
|
||
* @param {string} [params.order] 'ASC' | 'DESC', default 'DESC'
|
||
* @param {number} [params.*_min] Numeric range filter (e.g. hp_price_min)
|
||
* @param {number} [params.*_max] Numeric range filter (e.g. hp_price_max)
|
||
* @param {*} [params.*] Exact match filter (e.g. hp_featured: 1)
|
||
* @return {Promise<{items: Array, total: number, totalPages: number}>}
|
||
*/
|
||
WpdoClient.prototype.getListings = function ( params ) {
|
||
var merged = Object.assign( { post_type: this._postType }, params || {} );
|
||
return this._fetch( '/listings', merged ).then(
|
||
function ( res ) {
|
||
return {
|
||
items: res.data,
|
||
total: parseInt( res.headers.get( 'X-WP-Total' ) || '0', 10 ),
|
||
totalPages: parseInt( res.headers.get( 'X-WP-TotalPages' ) || '0', 10 ),
|
||
status: res.status,
|
||
};
|
||
}
|
||
);
|
||
};
|
||
|
||
/**
|
||
* Fetch a single listing (Zone A + Zone C merged).
|
||
*
|
||
* @param {number} id Post ID
|
||
* @return {Promise<object>}
|
||
*/
|
||
WpdoClient.prototype.getListing = function ( id ) {
|
||
return this._fetch( '/listings/' + id, null ).then(
|
||
function ( res ) {
|
||
return res.data;
|
||
}
|
||
);
|
||
};
|
||
|
||
/**
|
||
* Fetch Zone B view count for a post.
|
||
*
|
||
* @param {number} id Post ID
|
||
* @return {Promise<{post_id: number, view_count: number}>}
|
||
*/
|
||
WpdoClient.prototype.getStats = function ( id ) {
|
||
return this._fetch( '/stats/' + id, null ).then(
|
||
function ( res ) {
|
||
return res.data;
|
||
}
|
||
);
|
||
};
|
||
|
||
/**
|
||
* Increment Zone B view count for a post (requires WP REST nonce).
|
||
*
|
||
* The nonce must be passed via the `nonce` constructor option or
|
||
* via `wpdo_sdk_config.nonce` (wp_create_nonce('wp_rest')).
|
||
*
|
||
* @param {number} id Post ID
|
||
* @return {Promise<{post_id: number, view_count: number}>}
|
||
*
|
||
* Example:
|
||
* // Auto-tracks a view when a listing page loads:
|
||
* document.addEventListener('DOMContentLoaded', () => {
|
||
* const postId = parseInt(document.body.dataset.postId);
|
||
* if (postId) wpdo.incrementView(postId);
|
||
* });
|
||
*/
|
||
WpdoClient.prototype.incrementView = function ( id ) {
|
||
return this._post( '/listings/' + id + '/view', null ).then(
|
||
function ( res ) {
|
||
return res.data;
|
||
}
|
||
);
|
||
};
|
||
|
||
/**
|
||
* Async generator — iterates every page of listings.
|
||
*
|
||
* @param {object} [params] Same params as getListings (page is managed internally)
|
||
* @yields {{items: Array, total: number, page: number, totalPages: number}}
|
||
*
|
||
* Example:
|
||
* for await (const page of wpdo.paginateListings({ hp_featured: 1 })) {
|
||
* page.items.forEach(item => console.log(item));
|
||
* }
|
||
*/
|
||
WpdoClient.prototype.paginateListings = async function * ( params ) {
|
||
var page = 1;
|
||
var total = Infinity;
|
||
var perPage = ( params && params.per_page ) ? params.per_page : 20;
|
||
|
||
while ( ( page - 1 ) * perPage < total ) {
|
||
var merged = Object.assign( {}, params || {}, { page: page } );
|
||
var result = await this.getListings( merged );
|
||
total = result.total;
|
||
yield { items: result.items, total: total, page: page, totalPages: result.totalPages };
|
||
if ( page >= result.totalPages ) {
|
||
break;
|
||
}
|
||
page++;
|
||
}
|
||
};
|
||
|
||
// ── Export ────────────────────────────────────────────────────────────────
|
||
|
||
global.WpdoClient = WpdoClient;
|
||
|
||
// Auto-instantiate as window.wpdo if config is present.
|
||
if ( typeof wpdo_sdk_config !== 'undefined' ) {
|
||
global.wpdo = new WpdoClient();
|
||
}
|
||
|
||
}( window ) );
|