feat: add loading spinners and save feedback to admin UI
- Save Settings shows "Saving…" on button + green success banner after redirect (auto-dismisses after 6s) - Test Connections shows spinning icon + progress banner while in-flight - Generate Now shows spinning icon + timed pipeline stage messages so the user knows the pipeline is working, not hung - Shared ab-btn-loading CSS class with smooth dashicon spin animation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -54,6 +54,53 @@
|
|||||||
.ab-btn-outline:hover { border-color: var(--ab-primary); color: var(--ab-primary); background: var(--ab-primary-light); }
|
.ab-btn-outline:hover { border-color: var(--ab-primary); color: var(--ab-primary); background: var(--ab-primary-light); }
|
||||||
.ab-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
.ab-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||||
|
|
||||||
|
/* ── Button loading state ───────────────────────────────────────────── */
|
||||||
|
.ab-btn-loading .dashicons { animation: ab-spin 1s linear infinite; }
|
||||||
|
@keyframes ab-spin { to { transform: rotate(360deg); } }
|
||||||
|
.ab-btn-loading { pointer-events: none; opacity: 0.85; }
|
||||||
|
.ab-btn-loading .ab-btn-label { opacity: 0.9; }
|
||||||
|
|
||||||
|
/* ── Save success / error notice ────────────────────────────────────── */
|
||||||
|
.ab-save-notice {
|
||||||
|
margin: 16px 0 0; padding: 12px 16px; border-radius: var(--ab-radius);
|
||||||
|
border-left: 4px solid var(--ab-success); background: var(--ab-card-bg);
|
||||||
|
font-size: 13px; box-shadow: var(--ab-shadow); display: flex;
|
||||||
|
align-items: center; gap: 8px; animation: ab-slide-in 0.3s ease;
|
||||||
|
}
|
||||||
|
.ab-save-notice.notice-error { border-left-color: var(--ab-danger); }
|
||||||
|
.ab-save-notice .dashicons {
|
||||||
|
font-size: 18px; width: 18px; height: 18px; line-height: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.ab-save-notice .dashicons-saved { color: var(--ab-success); }
|
||||||
|
.ab-save-notice .dashicons-warning { color: var(--ab-danger); }
|
||||||
|
@keyframes ab-slide-in {
|
||||||
|
from { opacity: 0; transform: translateY(-8px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Pipeline progress stage ────────────────────────────────────────── */
|
||||||
|
.ab-progress-stage {
|
||||||
|
margin: 16px 0; padding: 14px 18px; border-radius: var(--ab-radius);
|
||||||
|
background: var(--ab-primary-light); border: 1px solid #c7d2fe;
|
||||||
|
font-size: 13px; color: var(--ab-primary); font-weight: 500;
|
||||||
|
display: flex; align-items: center; gap: 10px;
|
||||||
|
animation: ab-slide-in 0.3s ease;
|
||||||
|
}
|
||||||
|
.ab-progress-stage .dashicons {
|
||||||
|
font-size: 16px; width: 16px; height: 16px; line-height: 16px;
|
||||||
|
animation: ab-spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
.ab-progress-dots::after {
|
||||||
|
content: ''; animation: ab-dots 1.5s steps(3, end) infinite;
|
||||||
|
}
|
||||||
|
@keyframes ab-dots {
|
||||||
|
0% { content: ''; }
|
||||||
|
33% { content: '.'; }
|
||||||
|
66% { content: '..'; }
|
||||||
|
100% { content: '...'; }
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Stats bar ───────────────────────────────────────────────────────── */
|
/* ── Stats bar ───────────────────────────────────────────────────────── */
|
||||||
.ab-stats-bar {
|
.ab-stats-bar {
|
||||||
display: flex; gap: 1px; margin: 20px 0 0; background: var(--ab-border);
|
display: flex; gap: 1px; margin: 20px 0 0; background: var(--ab-border);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* PRAutoBlogger admin JavaScript.
|
* PRAutoBlogger admin JavaScript.
|
||||||
*
|
*
|
||||||
* Handles: tab switching, "Generate Now", "Test Connections", review queue actions.
|
* Handles: tab switching, "Generate Now" with pipeline progress stages,
|
||||||
|
* "Test Connections" with spinner, review queue actions, save feedback.
|
||||||
* Uses vanilla jQuery (WordPress admin already loads it).
|
* Uses vanilla jQuery (WordPress admin already loads it).
|
||||||
*
|
*
|
||||||
* @see admin/class-admin-page.php — Localizes prautobloggerAdmin object.
|
* @see admin/class-admin-page.php — Localizes prautobloggerAdmin object.
|
||||||
@@ -37,6 +38,29 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ── Save Settings feedback ─────────────────────────────────────────
|
||||||
|
* Shows "Saving…" on submit, auto-hides success notice after a delay.
|
||||||
|
*/
|
||||||
|
$(document).on('submit', '#ab-settings-form', function () {
|
||||||
|
var $btn = $(this).find('input[type="submit"]');
|
||||||
|
$btn.val('Saving…').prop('disabled', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-dismiss save notice after 6 seconds.
|
||||||
|
$(function () {
|
||||||
|
var $notice = $('#ab-save-notice');
|
||||||
|
if ($notice.length) {
|
||||||
|
setTimeout(function () {
|
||||||
|
$notice.fadeOut(400, function () { $(this).remove(); });
|
||||||
|
}, 6000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ── Helpers ─────────────────────────────────────────────────────────
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show a status message in the admin page.
|
* Show a status message in the admin page.
|
||||||
*
|
*
|
||||||
@@ -50,16 +74,81 @@
|
|||||||
if (type === 'error') {
|
if (type === 'error') {
|
||||||
$el.addClass('error');
|
$el.addClass('error');
|
||||||
}
|
}
|
||||||
|
hideProgress();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle "Generate Now" button click.
|
* Show a progress stage message with spinner.
|
||||||
|
*
|
||||||
|
* @param {string} message The stage text to display.
|
||||||
*/
|
*/
|
||||||
|
function showProgress(message) {
|
||||||
|
var $el = $('#prautoblogger-progress-stage');
|
||||||
|
$el.html(
|
||||||
|
'<span class="dashicons dashicons-update"></span>' +
|
||||||
|
'<span>' + message + '</span>'
|
||||||
|
).show();
|
||||||
|
// Hide the status message while progress is shown.
|
||||||
|
$('#prautoblogger-status-message').addClass('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hide the progress stage message. */
|
||||||
|
function hideProgress() {
|
||||||
|
$('#prautoblogger-progress-stage').hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a button into loading state with spinner.
|
||||||
|
*
|
||||||
|
* @param {jQuery} $btn The button element.
|
||||||
|
* @param {string} text The loading text to show.
|
||||||
|
*/
|
||||||
|
function setButtonLoading($btn, text) {
|
||||||
|
$btn.addClass('ab-btn-loading')
|
||||||
|
.prop('disabled', true);
|
||||||
|
$btn.find('.ab-btn-label').text(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset a button back to its default state.
|
||||||
|
*
|
||||||
|
* @param {jQuery} $btn The button element.
|
||||||
|
* @param {string} text The default text to restore.
|
||||||
|
*/
|
||||||
|
function resetButton($btn, text) {
|
||||||
|
$btn.removeClass('ab-btn-loading')
|
||||||
|
.prop('disabled', false);
|
||||||
|
$btn.find('.ab-btn-label').text(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ── Generate Now ───────────────────────────────────────────────────
|
||||||
|
* Shows pipeline stages while the request is in-flight so the user
|
||||||
|
* knows it hasn't hung.
|
||||||
|
*/
|
||||||
|
var generateStages = [
|
||||||
|
{ text: 'Collecting sources from Reddit…', delay: 0 },
|
||||||
|
{ text: 'Analyzing topics and scoring…', delay: 8000 },
|
||||||
|
{ text: 'Selecting best topic…', delay: 16000 },
|
||||||
|
{ text: 'Generating article draft via AI…', delay: 24000 },
|
||||||
|
{ text: 'Running editorial pass…', delay: 50000 },
|
||||||
|
{ text: 'Saving and publishing…', delay: 75000 },
|
||||||
|
{ text: 'Almost done — finalizing…', delay: 100000 }
|
||||||
|
];
|
||||||
|
var stageTimers = [];
|
||||||
|
|
||||||
$(document).on('click', '#prautoblogger-generate-now', function () {
|
$(document).on('click', '#prautoblogger-generate-now', function () {
|
||||||
var $btn = $(this);
|
var $btn = $(this);
|
||||||
if ($btn.prop('disabled')) return;
|
if ($btn.prop('disabled')) return;
|
||||||
|
|
||||||
$btn.prop('disabled', true).text(config.generatingText || 'Generating...');
|
setButtonLoading($btn, config.generatingText || 'Generating…');
|
||||||
|
|
||||||
|
// Start pipeline stage messages.
|
||||||
|
stageTimers = [];
|
||||||
|
$.each(generateStages, function (i, stage) {
|
||||||
|
var t = setTimeout(function () { showProgress(stage.text); }, stage.delay);
|
||||||
|
stageTimers.push(t);
|
||||||
|
});
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: config.ajaxUrl,
|
url: config.ajaxUrl,
|
||||||
@@ -93,19 +182,24 @@
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
complete: function () {
|
complete: function () {
|
||||||
$btn.prop('disabled', false).text(config.generateText || 'Generate Now');
|
// Clear all stage timers.
|
||||||
|
$.each(stageTimers, function (_, t) { clearTimeout(t); });
|
||||||
|
stageTimers = [];
|
||||||
|
hideProgress();
|
||||||
|
resetButton($btn, config.generateText || 'Generate Now');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/*
|
||||||
* Handle "Test Connections" button click.
|
* ── Test Connections ───────────────────────────────────────────────
|
||||||
*/
|
*/
|
||||||
$(document).on('click', '#prautoblogger-test-connection', function () {
|
$(document).on('click', '#prautoblogger-test-connection', function () {
|
||||||
var $btn = $(this);
|
var $btn = $(this);
|
||||||
if ($btn.prop('disabled')) return;
|
if ($btn.prop('disabled')) return;
|
||||||
|
|
||||||
$btn.prop('disabled', true).text(config.testingText || 'Testing...');
|
setButtonLoading($btn, config.testingText || 'Testing…');
|
||||||
|
showProgress('Testing API connections…');
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: config.ajaxUrl,
|
url: config.ajaxUrl,
|
||||||
@@ -133,7 +227,7 @@
|
|||||||
showStatus('Connection test request failed.', 'error');
|
showStatus('Connection test request failed.', 'error');
|
||||||
},
|
},
|
||||||
complete: function () {
|
complete: function () {
|
||||||
$btn.prop('disabled', false).text(config.testText || 'Test Connections');
|
resetButton($btn, config.testText || 'Test Connections');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -165,7 +259,7 @@
|
|||||||
var postId = $btn.data('post-id');
|
var postId = $btn.data('post-id');
|
||||||
if ($btn.prop('disabled')) return;
|
if ($btn.prop('disabled')) return;
|
||||||
|
|
||||||
$btn.prop('disabled', true).text('Publishing...');
|
$btn.prop('disabled', true).text('Publishing…');
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: config.ajaxUrl,
|
url: config.ajaxUrl,
|
||||||
@@ -202,7 +296,7 @@
|
|||||||
|
|
||||||
if (!confirm('Reject this post? It will be moved to trash.')) return;
|
if (!confirm('Reject this post? It will be moved to trash.')) return;
|
||||||
|
|
||||||
$btn.prop('disabled', true).text('Rejecting...');
|
$btn.prop('disabled', true).text('Rejecting…');
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: config.ajaxUrl,
|
url: config.ajaxUrl,
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ $next_run = wp_next_scheduled( 'prautoblogger_daily_generation' );
|
|||||||
<div class="ab-header-actions">
|
<div class="ab-header-actions">
|
||||||
<button type="button" id="prautoblogger-test-connection" class="ab-btn ab-btn-outline">
|
<button type="button" id="prautoblogger-test-connection" class="ab-btn ab-btn-outline">
|
||||||
<span class="dashicons dashicons-yes-alt"></span>
|
<span class="dashicons dashicons-yes-alt"></span>
|
||||||
<?php esc_html_e( 'Test Connections', 'prautoblogger' ); ?>
|
<span class="ab-btn-label"><?php esc_html_e( 'Test Connections', 'prautoblogger' ); ?></span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" id="prautoblogger-generate-now" class="ab-btn ab-btn-primary">
|
<button type="button" id="prautoblogger-generate-now" class="ab-btn ab-btn-primary">
|
||||||
<span class="dashicons dashicons-update"></span>
|
<span class="dashicons dashicons-update"></span>
|
||||||
<?php esc_html_e( 'Generate Now', 'prautoblogger' ); ?>
|
<span class="ab-btn-label"><?php esc_html_e( 'Generate Now', 'prautoblogger' ); ?></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,6 +66,20 @@ $next_run = wp_next_scheduled( 'prautoblogger_daily_generation' );
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if ( isset( $_GET['settings-updated'] ) && 'true' === $_GET['settings-updated'] ) : ?>
|
||||||
|
<div class="ab-save-notice" id="ab-save-notice">
|
||||||
|
<span class="dashicons dashicons-saved"></span>
|
||||||
|
<?php esc_html_e( 'Settings saved successfully.', 'prautoblogger' ); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ( isset( $_GET['error'] ) ) : ?>
|
||||||
|
<div class="ab-save-notice notice-error" id="ab-save-notice">
|
||||||
|
<span class="dashicons dashicons-warning"></span>
|
||||||
|
<?php esc_html_e( 'There was a problem saving your settings. Please try again.', 'prautoblogger' ); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div id="prautoblogger-progress-stage" class="ab-progress-stage" style="display:none;"></div>
|
||||||
<div id="prautoblogger-status-message" class="hidden"></div>
|
<div id="prautoblogger-status-message" class="hidden"></div>
|
||||||
|
|
||||||
<!-- Tab navigation + settings form -->
|
<!-- Tab navigation + settings form -->
|
||||||
|
|||||||
Reference in New Issue
Block a user