diff --git a/assets/css/admin.css b/assets/css/admin.css index b93be6e..f325960 100755 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -54,6 +54,53 @@ .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; } +/* ── 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 ───────────────────────────────────────────────────────── */ .ab-stats-bar { display: flex; gap: 1px; margin: 20px 0 0; background: var(--ab-border); diff --git a/assets/js/admin.js b/assets/js/admin.js index 5151263..5e8e247 100755 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -1,7 +1,8 @@ /** * 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). * * @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. * @@ -50,16 +74,81 @@ if (type === '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( + '' + + '' + message + '' + ).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 () { var $btn = $(this); 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({ url: config.ajaxUrl, @@ -93,19 +182,24 @@ ); }, 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 () { var $btn = $(this); if ($btn.prop('disabled')) return; - $btn.prop('disabled', true).text(config.testingText || 'Testing...'); + setButtonLoading($btn, config.testingText || 'Testing…'); + showProgress('Testing API connections…'); $.ajax({ url: config.ajaxUrl, @@ -133,7 +227,7 @@ showStatus('Connection test request failed.', 'error'); }, 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'); if ($btn.prop('disabled')) return; - $btn.prop('disabled', true).text('Publishing...'); + $btn.prop('disabled', true).text('Publishing…'); $.ajax({ url: config.ajaxUrl, @@ -202,7 +296,7 @@ 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({ url: config.ajaxUrl, diff --git a/templates/admin/settings-page.php b/templates/admin/settings-page.php index f52c554..879fd35 100755 --- a/templates/admin/settings-page.php +++ b/templates/admin/settings-page.php @@ -34,11 +34,11 @@ $next_run = wp_next_scheduled( 'prautoblogger_daily_generation' );