feat: add browsable peptide directory with category filtering, REST API, and extended meta fields (v4.3.0)

Adds [peptide_directory] shortcode with card grid, category taxonomy (8 terms),
search, pagination, detail modal, and /v1/compounds REST endpoint. Includes 9
new meta fields (half-life, stability, solubility, etc.), admin data tools for
migration/re-enrichment, integration hooks for cross-plugin extensibility,
dark mode support, and full uninstall cleanup. Refactors AI generator into
PSA_OpenRouter + PSA_KB_Builder classes. Adds PHPUnit tests for PostType and
Directory classes. Fixes QA findings: dead JS selector, renderError loading
state, numeric meta validation, prepared SQL in uninstall.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
peptiderepo
2026-04-13 21:30:11 +07:00
parent 0fa2707163
commit ed65b6ab90
28 changed files with 3380 additions and 783 deletions

View File

@@ -46,7 +46,7 @@
function showError(msg) {
hideAll();
$error.html('<strong>Error:</strong> ' + esc(msg)).show();
$error.html('<strong>' + esc(psaAjax.i18n.error) + '</strong> ' + esc(msg)).show();
$error.attr('role', 'alert');
}
@@ -58,12 +58,12 @@
function sourceBadge(source) {
var map = {
'pubchem': { label: 'Verified', cls: 'psa-badge-verified' },
'manual': { label: 'Curated', cls: 'psa-badge-manual' }
'pubchem': { label: psaAjax.i18n.verified, cls: 'psa-badge-verified' },
'manual': { label: psaAjax.i18n.curated, cls: 'psa-badge-manual' }
};
var b = map[source];
if (!b) return '';
return '<span class="psa-result-badge ' + b.cls + '">' + b.label + '</span>';
return '<span class="psa-result-badge ' + b.cls + '">' + esc(b.label) + '</span>';
}
/**
@@ -72,8 +72,7 @@
function renderResults(data) {
hideAll();
var html = '<p class="psa-result-count">' + data.total + ' peptide' +
(data.total !== 1 ? 's' : '') + ' found</p>';
var html = '<p class="psa-result-count">' + data.total + ' ' + esc(psaAjax.i18n.peptidesFound) + '</p>';
data.results.forEach(function (item) {
html += '<a href="' + esc(item.url) + '" class="psa-result-item">';
@@ -100,7 +99,7 @@
function showPending(name) {
hideAll();
$pending.find('.psa-pending-inner p').html(
'<strong>' + esc(name) + '</strong> is currently being added to our database. Please check back again later.'
'<strong>' + esc(name) + '</strong> ' + esc(psaAjax.i18n.pendingMsg)
);
$pending.show();
}
@@ -111,7 +110,7 @@
function showInvalid(name, reason) {
hideAll();
$invalid.find('.psa-invalid-text').html(
'<strong>' + esc(name) + '</strong> does not appear to be a recognized peptide. Please check the spelling or try a different search term.'
'<strong>' + esc(name) + '</strong> ' + esc(psaAjax.i18n.invalidMsg)
);
$invalid.show();
}
@@ -144,7 +143,7 @@
success: function (response) {
$results.attr('aria-busy', 'false');
if (!response.success) {
showError(response.data || 'Search failed.');
showError(response.data || psaAjax.i18n.searchFailed);
return;
}
var d = response.data;
@@ -159,24 +158,24 @@
showInvalid(query, d.message);
break;
case 'rate_limited':
showError(d.message || 'Too many requests. Please try again later.');
showError(d.message || psaAjax.i18n.rateLimited);
break;
default:
showError('Unexpected response.');
showError(psaAjax.i18n.unexpected);
}
},
error: function (jqXHR, textStatus, errorThrown) {
error: function (jqXHR, textStatus) {
$results.attr('aria-busy', 'false');
var errorMsg = 'Could not connect to the server. Please try again.';
var errorMsg = psaAjax.i18n.networkError;
if (textStatus === 'timeout') {
errorMsg = 'Request timed out. The server took too long to respond. Please try again.';
errorMsg = psaAjax.i18n.timeout;
} else if (textStatus === 'error') {
if (jqXHR.status === 0) {
errorMsg = 'Network error. Please check your connection and try again.';
errorMsg = psaAjax.i18n.connectionError;
} else if (jqXHR.status === 500) {
errorMsg = 'Server error. Please try again later.';
errorMsg = psaAjax.i18n.serverError;
} else if (jqXHR.status === 503) {
errorMsg = 'Server is temporarily unavailable. Please try again later.';
errorMsg = psaAjax.i18n.unavailable;
}
}
showError(errorMsg);