Merge BD Bookshelf (0.1.0), BD BookShelfHelper (2.0.0) and BD SimpleEmail
(2.0.0) into a single plugin released as Umbra Bookshelf 1.0.0, with the
BD Bookshelf theme left as a separate, unrenamed artifact.
Modules
includes/books/ catalogue, ISBN/ASIN, series, retail, ratings, shortcodes,
SEO, GDPR, scrapers, admin, CSV/post import
includes/library/ book post type + series taxonomy, pen names, data entry,
shortlinks, reviews, migrate
includes/email/ subscribers, newsletters, composition, sending
Renamed: plugin headers, text domains (all three -> umbra-bookshelf), classes
(BD_*/BDM_* -> Umbra_*), constants (-> UMBRA_BOOKSHELF_*), asset handles, JS
globals and admin slugs.
Deliberately not renamed -- persisted contract with live installs and with
published post content: table names, option keys, post meta keys, transients,
nonce actions, cron hooks, AJAX actions, shortcodes ([bd-book], [bd-books],
[bdm_signup]), post type `book`, taxonomy `series`, cookies and CSS classes.
BD_BookShelfHelper survives only as a class_alias for the unmodified theme.
Collisions resolved
* class-umbra-theme-compatibility.php declared BD_BookShelfHelper, the same
name as the library module's helper. Latent while the plugins were
separate (only fired when both were active), but an unconditional fatal
once merged. Renamed to Umbra_Theme_Compatibility with a class_alias.
* All three modules called add_menu_page. One top-level menu is now
registered by the bootstrap at admin_menu priority 9.
* BD Bookshelf and BookShelfHelper both claimed bd-bookshelf-settings, leaving
one Settings screen unreachable -> umbra-bookshelf-settings and
umbra-bookshelf-library-settings.
* Catalogue and library both enqueued bd-bookshelf-admin; the second enqueue
was silently discarded -> unique handles per module.
* Umbra_Admin and Umbra_Migration both registered bd_bookshelf_import_csv, so
one handler silently replaced the other. The import page now lives only in
Umbra_Admin (the variant wired to the admin JS); Umbra_Migration keeps only
the preview/run endpoints it uniquely provides.
* Modules were constructed from an init callback, so their own init
registrations raced the hook being dispatched. Now constructed on
plugins_loaded behind a re-entrancy guard.
Also
* Missing assets created: assets/images/default-cover.png (referenced but
never committed, so every cover-less book 404'd) and email/assets/js/
admin.js (enqueued on every email admin page but never committed).
* Library module's strpos($hook, 'bd-bookshelf') asset check matched every
catalogue and email screen once slugs shared the umbra-bookshelf- prefix;
now matched against its own slug list.
* Consolidated uninstall deduplicates the double drop of bd_books/bd_series.
* Specs aligned to the new name; pre-merge specs retained under
spec/archive/ with provenance notes.
* Tests added: tests/verify_structure.py (static) and tests/smoke_runtime.php
(WordPress-stub runtime boot, front-end and admin contexts).
Deferred and recorded in docs/KNOWN-ISSUES.md: the catalogue and library
modules still create bd_books and bd_series with two incompatible schemas
(pre-existing, now unconditional), and Goodreads/Amazon scraping is still
implemented twice on shared cron hooks.
1372 lines
64 KiB
PHP
1372 lines
64 KiB
PHP
<?php
|
|
// Copyright 2026 Umbra Tools (umbratools.com)
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
/**
|
|
* Admin Interface class
|
|
*
|
|
* @package Umbra_Bookshelf
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
class Umbra_Admin {
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
public function __construct() {
|
|
add_action('admin_menu', [$this, 'add_admin_menu']);
|
|
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_scripts']);
|
|
|
|
// AJAX handlers
|
|
add_action('wp_ajax_bd_bookshelf_save_book', [$this, 'ajax_save_book']);
|
|
add_action('wp_ajax_bd_bookshelf_delete_book', [$this, 'ajax_delete_book']);
|
|
add_action('wp_ajax_bd_bookshelf_get_author', [$this, 'ajax_get_author']);
|
|
add_action('wp_ajax_bd_bookshelf_save_author', [$this, 'ajax_save_author']);
|
|
add_action('wp_ajax_bd_bookshelf_delete_author', [$this, 'ajax_delete_author']);
|
|
add_action('wp_ajax_bd_bookshelf_get_series', [$this, 'ajax_get_series']);
|
|
add_action('wp_ajax_bd_bookshelf_save_series', [$this, 'ajax_save_series']);
|
|
add_action('wp_ajax_bd_bookshelf_delete_series', [$this, 'ajax_delete_series']);
|
|
add_action('wp_ajax_bd_bookshelf_get_shortcode', [$this, 'ajax_get_shortcode']);
|
|
add_action('wp_ajax_bd_bookshelf_save_shortcode', [$this, 'ajax_save_shortcode']);
|
|
add_action('wp_ajax_bd_bookshelf_delete_shortcode', [$this, 'ajax_delete_shortcode']);
|
|
add_action('wp_ajax_bd_bookshelf_save_sites', [$this, 'ajax_save_sites']);
|
|
add_action('wp_ajax_bd_bookshelf_save_settings', [$this, 'ajax_save_settings']);
|
|
add_action('wp_ajax_bd_bookshelf_import_csv', [$this, 'ajax_import_csv']);
|
|
add_action('wp_ajax_bd_bookshelf_import_posts', [$this, 'ajax_import_posts']);
|
|
add_action('wp_ajax_bd_bookshelf_sync_ratings', [$this, 'ajax_sync_ratings']);
|
|
}
|
|
|
|
/**
|
|
* Add admin menu
|
|
*/
|
|
public function add_admin_menu() {
|
|
// The top-level "Umbra Bookshelf" menu is registered once by the plugin
|
|
// bootstrap (Umbra_Bookshelf::register_admin_menu); each module only
|
|
// contributes submenus beneath it.
|
|
add_submenu_page('umbra-bookshelf', __('All Books', 'umbra-bookshelf'), __('All Books', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-all', [$this, 'render_books_page']);
|
|
add_submenu_page('umbra-bookshelf', __('Add New Book', 'umbra-bookshelf'), __('Add New Book', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-add', [$this, 'render_book_form']);
|
|
add_submenu_page('umbra-bookshelf', __('Authors', 'umbra-bookshelf'), __('Authors', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-authors', [$this, 'render_authors_page']);
|
|
add_submenu_page('umbra-bookshelf', __('Series', 'umbra-bookshelf'), __('Series', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-series', [$this, 'render_series_page']);
|
|
add_submenu_page('umbra-bookshelf', __('Retail Sites', 'umbra-bookshelf'), __('Retail Sites', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-sites', [$this, 'render_sites_page']);
|
|
add_submenu_page('umbra-bookshelf', __('Shortcodes', 'umbra-bookshelf'), __('Shortcodes', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-shortcodes', [$this, 'render_shortcodes_page']);
|
|
add_submenu_page('umbra-bookshelf', __('Rating Sync', 'umbra-bookshelf'), __('Rating Sync', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-sync', [$this, 'render_sync_page']);
|
|
add_submenu_page('umbra-bookshelf', __('Import', 'umbra-bookshelf'), __('Import', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-import', [$this, 'render_import_page']);
|
|
add_submenu_page('umbra-bookshelf', __('Settings', 'umbra-bookshelf'), __('Settings', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-settings', [$this, 'render_settings_page']);
|
|
}
|
|
|
|
/**
|
|
* Enqueue admin scripts
|
|
*/
|
|
public function enqueue_admin_scripts($hook) {
|
|
if (strpos($hook, 'umbra-bookshelf') === false) {
|
|
return;
|
|
}
|
|
|
|
wp_enqueue_media();
|
|
wp_enqueue_style('umbra-bookshelf-admin', UMBRA_BOOKSHELF_URL . 'admin/css/umbra-bookshelf-admin.css', [], UMBRA_BOOKSHELF_VERSION);
|
|
wp_enqueue_script('umbra-bookshelf-admin', UMBRA_BOOKSHELF_URL . 'admin/js/umbra-bookshelf-admin.js', ['jquery'], UMBRA_BOOKSHELF_VERSION, true);
|
|
|
|
wp_localize_script('umbra-bookshelf-admin', 'umbraBookshelfAdmin', [
|
|
'ajaxurl' => admin_url('admin-ajax.php'),
|
|
'nonce' => wp_create_nonce('bd_bookshelf_admin_nonce'),
|
|
'mediaTitle' => __('Select Cover Image', 'umbra-bookshelf'),
|
|
'mediaButton' => __('Use This Image', 'umbra-bookshelf'),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Render Books list page
|
|
*/
|
|
public function render_books_page() {
|
|
if (!current_user_can('manage_options')) {
|
|
wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
|
|
}
|
|
|
|
$books = Umbra_Book_Manager::get_books(['number' => 50]);
|
|
?>
|
|
<div class="wrap">
|
|
<h1 class="wp-heading-inline"><?php esc_html_e('Books', 'umbra-bookshelf'); ?></h1>
|
|
<a href="<?php echo admin_url('admin.php?page=bd-bookshelf-add'); ?>" class="page-title-action"><?php esc_html_e('Add New Book', 'umbra-bookshelf'); ?></a>
|
|
<hr class="wp-header-end">
|
|
|
|
<table class="wp-list-table widefat fixed striped">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:30%"><?php esc_html_e('Title', 'umbra-bookshelf'); ?></th>
|
|
<th style="width:15%"><?php esc_html_e('ISBNs', 'umbra-bookshelf'); ?></th>
|
|
<th style="width:15%"><?php esc_html_e('Ratings', 'umbra-bookshelf'); ?></th>
|
|
<th style="width:15%"><?php esc_html_e('Last Sync', 'umbra-bookshelf'); ?></th>
|
|
<th style="width:15%"><?php esc_html_e('Actions', 'umbra-bookshelf'); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($books)) : ?>
|
|
<tr><td colspan="5"><?php esc_html_e('No books found. Click "Add New Book" to get started.', 'umbra-bookshelf'); ?></td></tr>
|
|
<?php else : ?>
|
|
<?php foreach ($books as $book) : ?>
|
|
<tr>
|
|
<td>
|
|
<a href="<?php echo admin_url('admin.php?page=bd-bookshelf-add&book_id=' . $book->id); ?>">
|
|
<strong><?php echo esc_html($book->title); ?></strong>
|
|
</a>
|
|
</td>
|
|
<td>
|
|
<?php
|
|
$isbns = [];
|
|
if (!empty($book->isbn_ebook)) $isbns[] = 'eBook';
|
|
if (!empty($book->isbn_paperback)) $isbns[] = 'PB';
|
|
if (!empty($book->isbn_hardcover)) $isbns[] = 'HC';
|
|
if (!empty($book->isbn_audio)) $isbns[] = 'Audio';
|
|
echo esc_html(implode(', ', $isbns));
|
|
?>
|
|
</td>
|
|
<td>
|
|
Amazon: <?php echo number_format($book->rating_amazon, 1); ?><br>
|
|
Goodreads: <?php echo number_format($book->rating_goodreads, 1); ?>
|
|
</td>
|
|
<td><?php echo $book->last_sync ? date('M j, Y g:i a', strtotime($book->last_sync)) : '-'; ?></td>
|
|
<td>
|
|
<a href="<?php echo admin_url('admin.php?page=bd-bookshelf-add&book_id=' . $book->id); ?>"><?php esc_html_e('Edit', 'umbra-bookshelf'); ?></a> |
|
|
<a href="#" class="bd-bookshelf-delete" data-id="<?php echo $book->id; ?>" style="color:#a00"><?php esc_html_e('Delete', 'umbra-bookshelf'); ?></a>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render Book form
|
|
*/
|
|
public function render_book_form() {
|
|
if (!current_user_can('manage_options')) {
|
|
wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
|
|
}
|
|
|
|
$book_id = isset($_GET['book_id']) ? absint($_GET['book_id']) : 0;
|
|
$book = $book_id ? Umbra_Book_Manager::get_book($book_id) : null;
|
|
$series = Umbra_Series::get_series(['number' => 100]);
|
|
$authors = Umbra_Authors::get_authors(['number' => 100]);
|
|
|
|
// Get existing retail links grouped by format and site
|
|
$existing_links = [];
|
|
if ($book) {
|
|
$raw_links = Umbra_Book_Manager::get_book_retail_links($book_id);
|
|
foreach ($raw_links as $link) {
|
|
$existing_links[$link->format][$link->site_name] = $link->url;
|
|
}
|
|
}
|
|
|
|
// Retail sites for URL inputs
|
|
$retail_sites = Umbra_Retail_Sites::get_sites(true);
|
|
?>
|
|
<div class="wrap">
|
|
<h1><?php echo $book ? esc_html__('Edit Book', 'umbra-bookshelf') : esc_html__('Add New Book', 'umbra-bookshelf'); ?></h1>
|
|
|
|
<form id="bd-bookshelf-book-form">
|
|
<?php wp_nonce_field('bd_bookshelf_save_book', 'nonce'); ?>
|
|
<input type="hidden" name="book_id" value="<?php echo $book_id; ?>">
|
|
|
|
<h2><?php esc_html_e('Book Details', 'umbra-bookshelf'); ?></h2>
|
|
<table class="form-table">
|
|
<tr>
|
|
<th><label for="title"><?php esc_html_e('Title', 'umbra-bookshelf'); ?> <span class="required">*</span></label></th>
|
|
<td><input type="text" name="title" id="title" value="<?php echo $book ? esc_attr($book->title) : ''; ?>" class="regular-text" required></td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="description"><?php esc_html_e('Description', 'umbra-bookshelf'); ?></label></th>
|
|
<td><textarea name="description" id="description" rows="5" class="large-text"><?php echo $book ? esc_textarea($book->description) : ''; ?></textarea></td>
|
|
</tr>
|
|
<tr>
|
|
<th><label><?php esc_html_e('Cover Image', 'umbra-bookshelf'); ?></label></th>
|
|
<td>
|
|
<div class="bd-bookshelf-cover-upload">
|
|
<input type="hidden" name="cover_image" id="cover_image" value="<?php echo $book ? esc_url($book->cover_image) : ''; ?>">
|
|
<div id="cover-preview" style="margin-bottom:10px">
|
|
<?php if ($book && !empty($book->cover_image)) : ?>
|
|
<img src="<?php echo esc_url($book->cover_image); ?>" style="max-width:200px;height:auto">
|
|
<?php endif; ?>
|
|
</div>
|
|
<button type="button" id="cover-upload-btn" class="button"><?php esc_html_e('Select or Upload Cover', 'umbra-bookshelf'); ?></button>
|
|
<button type="button" id="cover-remove-btn" class="button" style="display:none;color:#a00"><?php esc_html_e('Remove', 'umbra-bookshelf'); ?></button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="series_id"><?php esc_html_e('Series', 'umbra-bookshelf'); ?></label></th>
|
|
<td>
|
|
<select name="series_id" id="series_id">
|
|
<option value="0"><?php esc_html_e('None', 'umbra-bookshelf'); ?></option>
|
|
<?php foreach ($series as $s) : ?>
|
|
<option value="<?php echo $s->id; ?>" <?php selected($book->series_id ?? 0, $s->id); ?>><?php echo esc_html($s->name); ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<a href="<?php echo admin_url('admin.php?page=bd-bookshelf-series'); ?>" target="_blank" class="button button-small"><?php esc_html_e('Manage Series', 'umbra-bookshelf'); ?></a>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<h2><?php esc_html_e('Authors', 'umbra-bookshelf'); ?></h2>
|
|
<p class="description"><?php esc_html_e('Select up to 3 authors for this book.', 'umbra-bookshelf'); ?></p>
|
|
<div id="bd-bookshelf-authors-container">
|
|
<?php
|
|
$book_authors = [];
|
|
if ($book && !empty($book->authors)) {
|
|
foreach ($book->authors as $ba) {
|
|
$book_authors[] = $ba->author_id;
|
|
}
|
|
}
|
|
for ($i = 0; $i < 3; $i++) :
|
|
$selected = $book_authors[$i] ?? 0;
|
|
?>
|
|
<div class="bd-bookshelf-author-row">
|
|
<select name="authors[]" class="bd-bookshelf-author-select">
|
|
<option value="0"><?php esc_html_e('-- Select Author --', 'umbra-bookshelf'); ?></option>
|
|
<?php foreach ($authors as $a) : ?>
|
|
<option value="<?php echo $a->id; ?>" <?php selected($selected, $a->id); ?>><?php echo esc_html($a->display_name); ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<?php endfor; ?>
|
|
</div>
|
|
<a href="<?php echo admin_url('admin.php?page=bd-bookshelf-authors'); ?>" target="_blank" class="button button-small"><?php esc_html_e('Manage Authors', 'umbra-bookshelf'); ?></a>
|
|
|
|
<h2><?php esc_html_e('ISBNs & ASINs', 'umbra-bookshelf'); ?></h2>
|
|
<table class="wp-list-table widefat fixed striped bd-bookshelf-isbn-grid">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:120px"><?php esc_html_e('Format', 'umbra-bookshelf'); ?></th>
|
|
<th><?php esc_html_e('ISBN', 'umbra-bookshelf'); ?></th>
|
|
<th><?php esc_html_e('Amazon ASIN', 'umbra-bookshelf'); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><strong><?php esc_html_e('eBook', 'umbra-bookshelf'); ?></strong></td>
|
|
<td><input type="text" name="isbn_ebook" value="<?php echo $book ? esc_attr($book->isbn_ebook) : ''; ?>" class="regular-text"></td>
|
|
<td><input type="text" name="asin_ebook" value="<?php echo $book ? esc_attr($book->asin_ebook ?? '') : ''; ?>" class="regular-text"></td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong><?php esc_html_e('Paperback', 'umbra-bookshelf'); ?></strong></td>
|
|
<td><input type="text" name="isbn_paperback" value="<?php echo $book ? esc_attr($book->isbn_paperback) : ''; ?>" class="regular-text"></td>
|
|
<td><input type="text" name="asin_paperback" value="<?php echo $book ? esc_attr($book->asin_paperback ?? '') : ''; ?>" class="regular-text"></td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong><?php esc_html_e('Hardcover', 'umbra-bookshelf'); ?></strong></td>
|
|
<td><input type="text" name="isbn_hardcover" value="<?php echo $book ? esc_attr($book->isbn_hardcover) : ''; ?>" class="regular-text"></td>
|
|
<td><input type="text" name="asin_hardcover" value="<?php echo $book ? esc_attr($book->asin_hardcover ?? '') : ''; ?>" class="regular-text"></td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong><?php esc_html_e('Audio', 'umbra-bookshelf'); ?></strong></td>
|
|
<td><input type="text" name="isbn_audio" value="<?php echo $book ? esc_attr($book->isbn_audio) : ''; ?>" class="regular-text"></td>
|
|
<td><input type="text" name="asin_audio" value="<?php echo $book ? esc_attr($book->asin_audio ?? '') : ''; ?>" class="regular-text"></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<input type="hidden" name="goodreads_id" value="<?php echo $book ? esc_attr($book->goodreads_id) : ''; ?>">
|
|
|
|
<h2><?php esc_html_e('Retail Links', 'umbra-bookshelf'); ?></h2>
|
|
<p class="description"><?php esc_html_e('Paste the direct purchase URL for each format and retailer. Leave blank if not available.', 'umbra-bookshelf'); ?></p>
|
|
|
|
<?php
|
|
$formats_config = [
|
|
'ebook' => [
|
|
'label' => 'eBook',
|
|
'sites' => ['Amazon', 'Apple Books', 'Google Play', 'Barnes & Noble', 'Kobo'],
|
|
],
|
|
'paperback' => [
|
|
'label' => 'Paperback',
|
|
'sites' => ['Amazon', 'Barnes & Noble', 'Kobo'],
|
|
],
|
|
'hardcover' => [
|
|
'label' => 'Hardcover',
|
|
'sites' => ['Amazon', 'Barnes & Noble', 'Kobo'],
|
|
],
|
|
'audio' => [
|
|
'label' => 'Audio',
|
|
'sites' => ['Amazon', 'Apple Books', 'Barnes & Noble', 'Kobo'],
|
|
],
|
|
];
|
|
?>
|
|
|
|
<?php foreach ($formats_config as $format_key => $format_config) : ?>
|
|
<h3><?php echo esc_html($format_config['label']); ?></h3>
|
|
<table class="form-table">
|
|
<?php foreach ($format_config['sites'] as $site_name) : ?>
|
|
<tr>
|
|
<th><label><?php echo esc_html($site_name); ?></label></th>
|
|
<td>
|
|
<input type="url"
|
|
name="retail_url[<?php echo esc_attr($format_key); ?>][<?php echo esc_attr($site_name); ?>]"
|
|
value="<?php echo esc_url($existing_links[$format_key][$site_name] ?? ''); ?>"
|
|
class="regular-text"
|
|
placeholder="https://...">
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</table>
|
|
<?php endforeach; ?>
|
|
|
|
<p class="submit">
|
|
<button type="submit" class="button button-primary button-hero"><?php esc_html_e('Save Book', 'umbra-bookshelf'); ?></button>
|
|
</p>
|
|
</form>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render Authors page
|
|
*/
|
|
public function render_authors_page() {
|
|
if (!current_user_can('manage_options')) {
|
|
wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
|
|
}
|
|
|
|
$authors = Umbra_Authors::get_authors(['number' => 100]);
|
|
?>
|
|
<div class="wrap">
|
|
<h1 class="wp-heading-inline"><?php esc_html_e('Authors', 'umbra-bookshelf'); ?></h1>
|
|
<button type="button" id="bd-bookshelf-add-author" class="page-title-action"><?php esc_html_e('Add New Author', 'umbra-bookshelf'); ?></button>
|
|
<hr class="wp-header-end">
|
|
|
|
<table class="wp-list-table widefat fixed striped">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:20%"><?php esc_html_e('Name', 'umbra-bookshelf'); ?></th>
|
|
<th style="width:20%"><?php esc_html_e('Email', 'umbra-bookshelf'); ?></th>
|
|
<th style="width:15%"><?php esc_html_e('Website', 'umbra-bookshelf'); ?></th>
|
|
<th style="width:15%"><?php esc_html_e('Goodreads ID', 'umbra-bookshelf'); ?></th>
|
|
<th style="width:15%"><?php esc_html_e('Actions', 'umbra-bookshelf'); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($authors)) : ?>
|
|
<tr><td colspan="5"><?php esc_html_e('No authors found.', 'umbra-bookshelf'); ?></td></tr>
|
|
<?php else : ?>
|
|
<?php foreach ($authors as $author) : ?>
|
|
<tr>
|
|
<td><strong><?php echo esc_html($author->display_name); ?></strong></td>
|
|
<td><?php echo esc_html($author->email); ?></td>
|
|
<td><?php echo $author->website ? '<a href="' . esc_url($author->website) . '" target="_blank">' . esc_html($author->website) . '</a>' : '-'; ?></td>
|
|
<td><?php echo esc_html($author->goodreads_author_id); ?></td>
|
|
<td>
|
|
<a href="#" class="bd-bookshelf-edit-author" data-id="<?php echo $author->id; ?>"><?php esc_html_e('Edit', 'umbra-bookshelf'); ?></a> |
|
|
<a href="#" class="bd-bookshelf-delete-author" data-id="<?php echo $author->id; ?>" style="color:#a00"><?php esc_html_e('Delete', 'umbra-bookshelf'); ?></a>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Author Modal -->
|
|
<div id="bd-bookshelf-author-modal" class="bd-bookshelf-modal" style="display:none">
|
|
<div class="bd-bookshelf-modal-content">
|
|
<div class="bd-bookshelf-modal-header">
|
|
<h2 id="bd-bookshelf-author-modal-title"><?php esc_html_e('Add New Author', 'umbra-bookshelf'); ?></h2>
|
|
<button type="button" class="bd-bookshelf-modal-close">×</button>
|
|
</div>
|
|
<form id="bd-bookshelf-author-form">
|
|
<?php wp_nonce_field('bd_bookshelf_admin_nonce', 'author_nonce'); ?>
|
|
<input type="hidden" name="author_id" id="author_id" value="0">
|
|
<table class="form-table">
|
|
<tr>
|
|
<th><label for="author_display_name"><?php esc_html_e('Display Name', 'umbra-bookshelf'); ?> <span class="required">*</span></label></th>
|
|
<td><input type="text" name="display_name" id="author_display_name" class="regular-text" required></td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="author_email"><?php esc_html_e('Email', 'umbra-bookshelf'); ?></label></th>
|
|
<td><input type="email" name="email" id="author_email" class="regular-text"></td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="author_website"><?php esc_html_e('Website', 'umbra-bookshelf'); ?></label></th>
|
|
<td><input type="url" name="website" id="author_website" class="regular-text"></td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="author_bio"><?php esc_html_e('Bio', 'umbra-bookshelf'); ?></label></th>
|
|
<td><textarea name="bio" id="author_bio" rows="4" class="large-text"></textarea></td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="author_goodreads_id"><?php esc_html_e('Goodreads Author ID', 'umbra-bookshelf'); ?></label></th>
|
|
<td><input type="text" name="goodreads_author_id" id="author_goodreads_id" class="regular-text"></td>
|
|
</tr>
|
|
<tr>
|
|
<th><label><?php esc_html_e('Photo', 'umbra-bookshelf'); ?></label></th>
|
|
<td>
|
|
<input type="hidden" name="photo_url" id="author_photo_url" value="">
|
|
<div id="author-photo-preview" style="margin-bottom:10px"></div>
|
|
<button type="button" id="author-photo-btn" class="button"><?php esc_html_e('Select Photo', 'umbra-bookshelf'); ?></button>
|
|
<button type="button" id="author-photo-remove" class="button" style="display:none;color:#a00"><?php esc_html_e('Remove', 'umbra-bookshelf'); ?></button>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
<p class="submit">
|
|
<button type="submit" class="button button-primary"><?php esc_html_e('Save Author', 'umbra-bookshelf'); ?></button>
|
|
</p>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render Series page
|
|
*/
|
|
public function render_series_page() {
|
|
if (!current_user_can('manage_options')) {
|
|
wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
|
|
}
|
|
|
|
$series = Umbra_Series::get_series(['number' => 100]);
|
|
?>
|
|
<div class="wrap">
|
|
<h1 class="wp-heading-inline"><?php esc_html_e('Series', 'umbra-bookshelf'); ?></h1>
|
|
<button type="button" id="bd-bookshelf-add-series" class="page-title-action"><?php esc_html_e('Add New Series', 'umbra-bookshelf'); ?></button>
|
|
<hr class="wp-header-end">
|
|
|
|
<table class="wp-list-table widefat fixed striped">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:30%"><?php esc_html_e('Name', 'umbra-bookshelf'); ?></th>
|
|
<th style="width:50%"><?php esc_html_e('Description', 'umbra-bookshelf'); ?></th>
|
|
<th style="width:20%"><?php esc_html_e('Actions', 'umbra-bookshelf'); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($series)) : ?>
|
|
<tr><td colspan="3"><?php esc_html_e('No series found.', 'umbra-bookshelf'); ?></td></tr>
|
|
<?php else : ?>
|
|
<?php foreach ($series as $s) : ?>
|
|
<tr>
|
|
<td><strong><?php echo esc_html($s->name); ?></strong></td>
|
|
<td><?php echo esc_html(wp_trim_words($s->description, 15)); ?></td>
|
|
<td>
|
|
<a href="#" class="bd-bookshelf-edit-series" data-id="<?php echo $s->id; ?>"><?php esc_html_e('Edit', 'umbra-bookshelf'); ?></a> |
|
|
<a href="#" class="bd-bookshelf-delete-series" data-id="<?php echo $s->id; ?>" style="color:#a00"><?php esc_html_e('Delete', 'umbra-bookshelf'); ?></a>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Series Modal -->
|
|
<div id="bd-bookshelf-series-modal" class="bd-bookshelf-modal" style="display:none">
|
|
<div class="bd-bookshelf-modal-content">
|
|
<div class="bd-bookshelf-modal-header">
|
|
<h2 id="bd-bookshelf-series-modal-title"><?php esc_html_e('Add New Series', 'umbra-bookshelf'); ?></h2>
|
|
<button type="button" class="bd-bookshelf-modal-close">×</button>
|
|
</div>
|
|
<form id="bd-bookshelf-series-form">
|
|
<?php wp_nonce_field('bd_bookshelf_admin_nonce', 'series_nonce'); ?>
|
|
<input type="hidden" name="series_id" id="series_id_input" value="0">
|
|
<table class="form-table">
|
|
<tr>
|
|
<th><label for="series_name"><?php esc_html_e('Series Name', 'umbra-bookshelf'); ?> <span class="required">*</span></label></th>
|
|
<td><input type="text" name="name" id="series_name" class="regular-text" required></td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="series_description"><?php esc_html_e('Description', 'umbra-bookshelf'); ?></label></th>
|
|
<td><textarea name="description" id="series_description" rows="4" class="large-text"></textarea></td>
|
|
</tr>
|
|
</table>
|
|
<p class="submit">
|
|
<button type="submit" class="button button-primary"><?php esc_html_e('Save Series', 'umbra-bookshelf'); ?></button>
|
|
</p>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render Sites page
|
|
*/
|
|
public function render_sites_page() {
|
|
if (!current_user_can('manage_options')) {
|
|
wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
|
|
}
|
|
|
|
$sites = Umbra_Retail_Sites::get_sites(false);
|
|
?>
|
|
<div class="wrap">
|
|
<h1><?php esc_html_e('Retail Sites & Affiliate Codes', 'umbra-bookshelf'); ?></h1>
|
|
<p class="description"><?php esc_html_e('Enter your affiliate codes for each retail site. These codes will be appended to book links.', 'umbra-bookshelf'); ?></p>
|
|
|
|
<form id="bd-bookshelf-sites-form">
|
|
<?php wp_nonce_field('bd_bookshelf_admin_nonce', 'sites_nonce'); ?>
|
|
|
|
<table class="wp-list-table widefat fixed striped">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:25%"><?php esc_html_e('Site', 'umbra-bookshelf'); ?></th>
|
|
<th style="width:35%"><?php esc_html_e('URL Pattern', 'umbra-bookshelf'); ?></th>
|
|
<th style="width:15%"><?php esc_html_e('Affiliate Param', 'umbra-bookshelf'); ?></th>
|
|
<th style="width:25%"><?php esc_html_e('Your Affiliate Code', 'umbra-bookshelf'); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($sites as $site) : ?>
|
|
<tr>
|
|
<td><strong><?php echo esc_html($site->name); ?></strong></td>
|
|
<td><code style="font-size:11px"><?php echo esc_html($site->url_pattern); ?></code></td>
|
|
<td><?php echo esc_html($site->affiliate_param); ?></td>
|
|
<td>
|
|
<input type="text" name="affiliate[<?php echo $site->id; ?>]" value="<?php echo esc_attr($site->affiliate_code); ?>" class="regular-text" placeholder="<?php esc_attr_e('Enter affiliate code', 'umbra-bookshelf'); ?>">
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
|
|
<p class="submit">
|
|
<button type="submit" class="button button-primary button-hero"><?php esc_html_e('Save Affiliate Codes', 'umbra-bookshelf'); ?></button>
|
|
</p>
|
|
</form>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render Shortcodes page
|
|
*/
|
|
public function render_shortcodes_page() {
|
|
if (!current_user_can('manage_options')) {
|
|
wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
|
|
}
|
|
|
|
global $wpdb;
|
|
$table = Umbra_Database::table('shortcodes');
|
|
$shortcodes = $wpdb->get_results("SELECT * FROM {$table} ORDER BY created_at DESC");
|
|
?>
|
|
<div class="wrap">
|
|
<h1 class="wp-heading-inline"><?php esc_html_e('Custom Shortcodes', 'umbra-bookshelf'); ?></h1>
|
|
<button type="button" id="bd-bookshelf-add-shortcode" class="page-title-action"><?php esc_html_e('Add New Shortcode', 'umbra-bookshelf'); ?></button>
|
|
<hr class="wp-header-end">
|
|
|
|
<p class="description"><?php esc_html_e('Create shortcodes that redirect to external URLs. Use [slug] in your posts to redirect visitors.', 'umbra-bookshelf'); ?></p>
|
|
|
|
<table class="wp-list-table widefat fixed striped">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:20%"><?php esc_html_e('Shortcode', 'umbra-bookshelf'); ?></th>
|
|
<th style="width:30%"><?php esc_html_e('Target URL', 'umbra-bookshelf'); ?></th>
|
|
<th style="width:15%"><?php esc_html_e('Redirect', 'umbra-bookshelf'); ?></th>
|
|
<th style="width:15%"><?php esc_html_e('Clicks', 'umbra-bookshelf'); ?></th>
|
|
<th style="width:20%"><?php esc_html_e('Actions', 'umbra-bookshelf'); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($shortcodes)) : ?>
|
|
<tr><td colspan="5"><?php esc_html_e('No shortcodes found.', 'umbra-bookshelf'); ?></td></tr>
|
|
<?php else : ?>
|
|
<?php foreach ($shortcodes as $sc) : ?>
|
|
<tr>
|
|
<td><code>[<?php echo esc_html($sc->slug); ?>]</code></td>
|
|
<td><a href="<?php echo esc_url($sc->target_url); ?>" target="_blank"><?php echo esc_html($sc->target_url); ?></a></td>
|
|
<td><?php echo esc_html($sc->redirect_type); ?></td>
|
|
<td><?php echo number_format($sc->click_count); ?></td>
|
|
<td>
|
|
<a href="#" class="bd-bookshelf-edit-shortcode" data-id="<?php echo $sc->id; ?>"><?php esc_html_e('Edit', 'umbra-bookshelf'); ?></a> |
|
|
<a href="#" class="bd-bookshelf-delete-shortcode" data-id="<?php echo $sc->id; ?>" style="color:#a00"><?php esc_html_e('Delete', 'umbra-bookshelf'); ?></a>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Shortcode Modal -->
|
|
<div id="bd-bookshelf-shortcode-modal" class="bd-bookshelf-modal" style="display:none">
|
|
<div class="bd-bookshelf-modal-content">
|
|
<div class="bd-bookshelf-modal-header">
|
|
<h2 id="bd-bookshelf-shortcode-modal-title"><?php esc_html_e('Add New Shortcode', 'umbra-bookshelf'); ?></h2>
|
|
<button type="button" class="bd-bookshelf-modal-close">×</button>
|
|
</div>
|
|
<form id="bd-bookshelf-shortcode-form">
|
|
<?php wp_nonce_field('bd_bookshelf_admin_nonce', 'shortcode_nonce'); ?>
|
|
<input type="hidden" name="shortcode_id" id="shortcode_id" value="0">
|
|
<table class="form-table">
|
|
<tr>
|
|
<th><label for="shortcode_slug"><?php esc_html_e('Shortcode Slug', 'umbra-bookshelf'); ?> <span class="required">*</span></label></th>
|
|
<td>
|
|
<input type="text" name="slug" id="shortcode_slug" class="regular-text" required pattern="[a-z0-9\-]+">
|
|
<p class="description"><?php esc_html_e('Only lowercase letters, numbers, and hyphens. Use as: [your-slug]', 'umbra-bookshelf'); ?></p>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="shortcode_target"><?php esc_html_e('Target URL', 'umbra-bookshelf'); ?> <span class="required">*</span></label></th>
|
|
<td><input type="url" name="target_url" id="shortcode_target" class="regular-text" required></td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="shortcode_redirect"><?php esc_html_e('Redirect Type', 'umbra-bookshelf'); ?></label></th>
|
|
<td>
|
|
<select name="redirect_type" id="shortcode_redirect">
|
|
<option value="301">301 - Permanent</option>
|
|
<option value="302">302 - Temporary</option>
|
|
<option value="307">307 - Temporary</option>
|
|
</select>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="shortcode_description"><?php esc_html_e('Description', 'umbra-bookshelf'); ?></label></th>
|
|
<td><input type="text" name="description" id="shortcode_description" class="regular-text"></td>
|
|
</tr>
|
|
</table>
|
|
<p class="submit">
|
|
<button type="submit" class="button button-primary"><?php esc_html_e('Save Shortcode', 'umbra-bookshelf'); ?></button>
|
|
</p>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render Sync page
|
|
*/
|
|
public function render_sync_page() {
|
|
if (!current_user_can('manage_options')) {
|
|
wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
|
|
}
|
|
|
|
$last_sync = Umbra_Rating_Sync::get_last_sync_info();
|
|
?>
|
|
<div class="wrap">
|
|
<h1><?php esc_html_e('Rating Sync', 'umbra-bookshelf'); ?></h1>
|
|
|
|
<div class="bd-bookshelf-import-section">
|
|
<h2><?php esc_html_e('Sync Status', 'umbra-bookshelf'); ?></h2>
|
|
|
|
<?php if (!empty($last_sync)) : ?>
|
|
<table class="form-table">
|
|
<tr>
|
|
<th><?php esc_html_e('Last Sync Time', 'umbra-bookshelf'); ?></th>
|
|
<td><?php echo esc_html($last_sync['time']); ?></td>
|
|
</tr>
|
|
<tr>
|
|
<th><?php esc_html_e('Books Synced', 'umbra-bookshelf'); ?></th>
|
|
<td><?php echo esc_html($last_sync['synced']); ?></td>
|
|
</tr>
|
|
<tr>
|
|
<th><?php esc_html_e('Books Failed', 'umbra-bookshelf'); ?></th>
|
|
<td><?php echo esc_html($last_sync['failed']); ?></td>
|
|
</tr>
|
|
<tr>
|
|
<th><?php esc_html_e('Total Books', 'umbra-bookshelf'); ?></th>
|
|
<td><?php echo esc_html($last_sync['total']); ?></td>
|
|
</tr>
|
|
</table>
|
|
<?php else : ?>
|
|
<p><?php esc_html_e('No sync has been performed yet.', 'umbra-bookshelf'); ?></p>
|
|
<?php endif; ?>
|
|
|
|
<p class="submit">
|
|
<button type="button" id="bd-bookshelf-manual-sync" class="button button-primary"><?php esc_html_e('Sync Now', 'umbra-bookshelf'); ?></button>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render Settings page
|
|
*/
|
|
public function render_settings_page() {
|
|
if (!current_user_can('manage_options')) {
|
|
wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
|
|
}
|
|
|
|
?>
|
|
<div class="wrap">
|
|
<h1><?php esc_html_e('Settings', 'umbra-bookshelf'); ?></h1>
|
|
|
|
<form id="bd-bookshelf-settings-form">
|
|
<?php wp_nonce_field('bd_bookshelf_save_settings', 'nonce'); ?>
|
|
|
|
<h2><?php esc_html_e('GDPR Settings', 'umbra-bookshelf'); ?></h2>
|
|
<table class="form-table">
|
|
<tr>
|
|
<th><label for="gdpr_text"><?php esc_html_e('Consent Banner Text', 'umbra-bookshelf'); ?></label></th>
|
|
<td>
|
|
<textarea name="gdpr_text" id="gdpr_text" rows="3" class="large-text"><?php echo esc_textarea(Umbra_GDPR::get_consent_text()); ?></textarea>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<p class="submit">
|
|
<button type="submit" class="button button-primary"><?php esc_html_e('Save Settings', 'umbra-bookshelf'); ?></button>
|
|
</p>
|
|
</form>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render Import page
|
|
*/
|
|
public function render_import_page() {
|
|
if (!current_user_can('manage_options')) {
|
|
wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
|
|
}
|
|
|
|
?>
|
|
<div class="wrap">
|
|
<h1><?php esc_html_e('Import Books', 'umbra-bookshelf'); ?></h1>
|
|
|
|
<div class="bd-bookshelf-import-section">
|
|
<h2><?php esc_html_e('Import from CSV', 'umbra-bookshelf'); ?></h2>
|
|
<p><?php esc_html_e('Import books from a CSV file. Required column: title. Optional columns: description, isbn_ebook, isbn_paperback, isbn_hardcover, isbn_audio, amazon_asin, goodreads_id.', 'umbra-bookshelf'); ?></p>
|
|
|
|
<form id="bd-bookshelf-import-csv-form" enctype="multipart/form-data">
|
|
<?php wp_nonce_field('bd_bookshelf_admin_nonce', 'import_nonce'); ?>
|
|
|
|
<table class="form-table">
|
|
<tr>
|
|
<th><label for="csv_file"><?php esc_html_e('CSV File', 'umbra-bookshelf'); ?></label></th>
|
|
<td>
|
|
<input type="file" name="csv_file" id="csv_file" accept=".csv" required>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<p class="submit">
|
|
<button type="submit" class="button button-primary"><?php esc_html_e('Import CSV', 'umbra-bookshelf'); ?></button>
|
|
</p>
|
|
</form>
|
|
|
|
<div id="bd-bookshelf-import-results" style="display:none">
|
|
<h3><?php esc_html_e('Import Results', 'umbra-bookshelf'); ?></h3>
|
|
<p id="bd-bookshelf-import-message"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bd-bookshelf-import-section">
|
|
<h2><?php esc_html_e('Import from WordPress Posts', 'umbra-bookshelf'); ?></h2>
|
|
<p><?php esc_html_e('Import books from existing WordPress posts or custom post types.', 'umbra-bookshelf'); ?></p>
|
|
|
|
<form id="bd-bookshelf-import-posts-form">
|
|
<?php wp_nonce_field('bd_bookshelf_admin_nonce', 'import_posts_nonce'); ?>
|
|
|
|
<table class="form-table">
|
|
<tr>
|
|
<th><label for="post_type"><?php esc_html_e('Post Type', 'umbra-bookshelf'); ?></label></th>
|
|
<td>
|
|
<select name="post_type" id="post_type">
|
|
<option value="post"><?php esc_html_e('Posts', 'umbra-bookshelf'); ?></option>
|
|
<option value="page"><?php esc_html_e('Pages', 'umbra-bookshelf'); ?></option>
|
|
<option value="book"><?php esc_html_e('Books (if available)', 'umbra-bookshelf'); ?></option>
|
|
</select>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="limit"><?php esc_html_e('Limit', 'umbra-bookshelf'); ?></label></th>
|
|
<td>
|
|
<input type="number" name="limit" id="limit" value="100" min="1" max="1000">
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<p class="submit">
|
|
<button type="submit" class="button button-primary"><?php esc_html_e('Import Posts', 'umbra-bookshelf'); ?></button>
|
|
</p>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for saving book
|
|
*/
|
|
public function ajax_save_book() {
|
|
check_ajax_referer('bd_bookshelf_save_book', 'nonce');
|
|
|
|
if (!current_user_can('manage_options')) {
|
|
wp_send_json_error(['message' => 'Insufficient permissions']);
|
|
}
|
|
|
|
$book_id = absint($_POST['book_id'] ?? 0);
|
|
|
|
$data = [
|
|
'title' => sanitize_text_field($_POST['title'] ?? ''),
|
|
'description' => wp_kses_post($_POST['description'] ?? ''),
|
|
'cover_image' => esc_url_raw($_POST['cover_image'] ?? ''),
|
|
'isbn_ebook' => sanitize_text_field($_POST['isbn_ebook'] ?? ''),
|
|
'isbn_paperback' => sanitize_text_field($_POST['isbn_paperback'] ?? ''),
|
|
'isbn_hardcover' => sanitize_text_field($_POST['isbn_hardcover'] ?? ''),
|
|
'isbn_audio' => sanitize_text_field($_POST['isbn_audio'] ?? ''),
|
|
'asin_ebook' => sanitize_text_field($_POST['asin_ebook'] ?? ''),
|
|
'asin_paperback' => sanitize_text_field($_POST['asin_paperback'] ?? ''),
|
|
'asin_hardcover' => sanitize_text_field($_POST['asin_hardcover'] ?? ''),
|
|
'asin_audio' => sanitize_text_field($_POST['asin_audio'] ?? ''),
|
|
'amazon_asin' => sanitize_text_field($_POST['asin_ebook'] ?? ''),
|
|
'goodreads_id' => sanitize_text_field($_POST['goodreads_id'] ?? ''),
|
|
'series_id' => absint($_POST['series_id'] ?? 0),
|
|
'author_ids' => array_map('absint', $_POST['authors'] ?? []),
|
|
];
|
|
|
|
if ($book_id) {
|
|
$result = Umbra_Book_Manager::update_book($book_id, $data);
|
|
} else {
|
|
$data['slug'] = sanitize_title($data['title']);
|
|
$book_id = Umbra_Book_Manager::create_book($data);
|
|
$result = $book_id !== false;
|
|
}
|
|
|
|
if ($result) {
|
|
// Save retail URLs
|
|
$retail_urls = $_POST['retail_url'] ?? [];
|
|
$this->save_retail_urls($book_id, $retail_urls);
|
|
|
|
wp_send_json_success(['message' => 'Book saved', 'id' => $book_id]);
|
|
} else {
|
|
wp_send_json_error(['message' => 'Failed to save book']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save retail URLs for a book
|
|
*/
|
|
private function save_retail_urls($book_id, $retail_urls) {
|
|
global $wpdb;
|
|
|
|
$links_table = Umbra_Database::table('retail_links');
|
|
$sites_table = Umbra_Database::table('retail_sites');
|
|
|
|
// Delete existing non-custom links
|
|
$wpdb->delete($links_table, ['book_id' => $book_id, 'is_custom' => 0], ['%d', '%d']);
|
|
|
|
if (empty($retail_urls) || !is_array($retail_urls)) {
|
|
return;
|
|
}
|
|
|
|
foreach ($retail_urls as $format => $sites) {
|
|
if (!is_array($sites)) continue;
|
|
|
|
foreach ($sites as $site_name => $url) {
|
|
$url = esc_url_raw(trim($url));
|
|
if (empty($url)) continue;
|
|
|
|
// Find or create site
|
|
$site = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT id FROM {$sites_table} WHERE name = %s",
|
|
$site_name
|
|
));
|
|
|
|
if (!$site) {
|
|
$wpdb->insert($sites_table, [
|
|
'name' => $site_name,
|
|
'url_pattern' => '',
|
|
'is_custom' => 1,
|
|
'is_active' => 1,
|
|
]);
|
|
$site_id = $wpdb->insert_id;
|
|
} else {
|
|
$site_id = $site->id;
|
|
}
|
|
|
|
$wpdb->insert($links_table, [
|
|
'book_id' => $book_id,
|
|
'site_id' => $site_id,
|
|
'format' => $format,
|
|
'url' => $url,
|
|
'is_custom' => 0,
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for deleting book
|
|
*/
|
|
public function ajax_delete_book() {
|
|
check_ajax_referer('bd_bookshelf_admin_nonce', 'nonce');
|
|
|
|
if (!current_user_can('manage_options')) {
|
|
wp_send_json_error(['message' => 'Insufficient permissions']);
|
|
}
|
|
|
|
$book_id = absint($_POST['id'] ?? 0);
|
|
if (!$book_id) {
|
|
wp_send_json_error(['message' => 'Invalid book ID']);
|
|
}
|
|
|
|
$result = Umbra_Book_Manager::delete_book($book_id);
|
|
|
|
if ($result) {
|
|
wp_send_json_success(['message' => 'Book deleted']);
|
|
} else {
|
|
wp_send_json_error(['message' => 'Failed to delete book']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for getting author
|
|
*/
|
|
public function ajax_get_author() {
|
|
check_ajax_referer('bd_bookshelf_admin_nonce', 'nonce');
|
|
|
|
if (!current_user_can('manage_options')) {
|
|
wp_send_json_error(['message' => 'Insufficient permissions']);
|
|
}
|
|
|
|
$author_id = absint($_POST['id'] ?? 0);
|
|
if (!$author_id) {
|
|
wp_send_json_error(['message' => 'Invalid author ID']);
|
|
}
|
|
|
|
$author = Umbra_Authors::get_author($author_id);
|
|
if (!$author) {
|
|
wp_send_json_error(['message' => 'Author not found']);
|
|
}
|
|
|
|
wp_send_json_success($author);
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for saving author
|
|
*/
|
|
public function ajax_save_author() {
|
|
check_ajax_referer('bd_bookshelf_admin_nonce', 'author_nonce');
|
|
|
|
if (!current_user_can('manage_options')) {
|
|
wp_send_json_error(['message' => 'Insufficient permissions']);
|
|
}
|
|
|
|
$author_id = absint($_POST['author_id'] ?? 0);
|
|
|
|
$data = [
|
|
'display_name' => sanitize_text_field($_POST['display_name'] ?? ''),
|
|
'email' => sanitize_email($_POST['email'] ?? ''),
|
|
'website' => esc_url_raw($_POST['website'] ?? ''),
|
|
'bio' => wp_kses_post($_POST['bio'] ?? ''),
|
|
'goodreads_author_id' => sanitize_text_field($_POST['goodreads_author_id'] ?? ''),
|
|
'photo_url' => esc_url_raw($_POST['photo_url'] ?? ''),
|
|
];
|
|
|
|
if ($author_id) {
|
|
$result = Umbra_Authors::update_author($author_id, $data);
|
|
} else {
|
|
$author_id = Umbra_Authors::create_author($data);
|
|
$result = $author_id !== false;
|
|
}
|
|
|
|
if ($result) {
|
|
wp_send_json_success(['message' => 'Author saved', 'id' => $author_id]);
|
|
} else {
|
|
wp_send_json_error(['message' => 'Failed to save author']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for deleting author
|
|
*/
|
|
public function ajax_delete_author() {
|
|
check_ajax_referer('bd_bookshelf_admin_nonce', 'nonce');
|
|
|
|
if (!current_user_can('manage_options')) {
|
|
wp_send_json_error(['message' => 'Insufficient permissions']);
|
|
}
|
|
|
|
$author_id = absint($_POST['id'] ?? 0);
|
|
if (!$author_id) {
|
|
wp_send_json_error(['message' => 'Invalid author ID']);
|
|
}
|
|
|
|
$result = Umbra_Authors::delete_author($author_id);
|
|
|
|
if ($result) {
|
|
wp_send_json_success(['message' => 'Author deleted']);
|
|
} else {
|
|
wp_send_json_error(['message' => 'Failed to delete author']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for getting series
|
|
*/
|
|
public function ajax_get_series() {
|
|
check_ajax_referer('bd_bookshelf_admin_nonce', 'nonce');
|
|
|
|
if (!current_user_can('manage_options')) {
|
|
wp_send_json_error(['message' => 'Insufficient permissions']);
|
|
}
|
|
|
|
$series_id = absint($_POST['id'] ?? 0);
|
|
if (!$series_id) {
|
|
wp_send_json_error(['message' => 'Invalid series ID']);
|
|
}
|
|
|
|
$series = Umbra_Series::get_series_by_id($series_id);
|
|
if (!$series) {
|
|
wp_send_json_error(['message' => 'Series not found']);
|
|
}
|
|
|
|
wp_send_json_success($series);
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for saving series
|
|
*/
|
|
public function ajax_save_series() {
|
|
check_ajax_referer('bd_bookshelf_admin_nonce', 'series_nonce');
|
|
|
|
if (!current_user_can('manage_options')) {
|
|
wp_send_json_error(['message' => 'Insufficient permissions']);
|
|
}
|
|
|
|
$series_id = absint($_POST['series_id'] ?? 0);
|
|
|
|
$data = [
|
|
'name' => sanitize_text_field($_POST['name'] ?? ''),
|
|
'description' => wp_kses_post($_POST['description'] ?? ''),
|
|
];
|
|
|
|
if ($series_id) {
|
|
$result = Umbra_Series::update_series($series_id, $data);
|
|
} else {
|
|
$series_id = Umbra_Series::create_series($data);
|
|
$result = $series_id !== false;
|
|
}
|
|
|
|
if ($result) {
|
|
wp_send_json_success(['message' => 'Series saved', 'id' => $series_id]);
|
|
} else {
|
|
wp_send_json_error(['message' => 'Failed to save series']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for deleting series
|
|
*/
|
|
public function ajax_delete_series() {
|
|
check_ajax_referer('bd_bookshelf_admin_nonce', 'nonce');
|
|
|
|
if (!current_user_can('manage_options')) {
|
|
wp_send_json_error(['message' => 'Insufficient permissions']);
|
|
}
|
|
|
|
$series_id = absint($_POST['id'] ?? 0);
|
|
if (!$series_id) {
|
|
wp_send_json_error(['message' => 'Invalid series ID']);
|
|
}
|
|
|
|
$result = Umbra_Series::delete_series($series_id);
|
|
|
|
if ($result) {
|
|
wp_send_json_success(['message' => 'Series deleted']);
|
|
} else {
|
|
wp_send_json_error(['message' => 'Failed to delete series']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for getting shortcode
|
|
*/
|
|
public function ajax_get_shortcode() {
|
|
check_ajax_referer('bd_bookshelf_admin_nonce', 'nonce');
|
|
|
|
if (!current_user_can('manage_options')) {
|
|
wp_send_json_error(['message' => 'Insufficient permissions']);
|
|
}
|
|
|
|
$shortcode_id = absint($_POST['id'] ?? 0);
|
|
if (!$shortcode_id) {
|
|
wp_send_json_error(['message' => 'Invalid shortcode ID']);
|
|
}
|
|
|
|
global $wpdb;
|
|
$table = Umbra_Database::table('shortcodes');
|
|
$shortcode = $wpdb->get_row(
|
|
$wpdb->prepare(
|
|
"SELECT * FROM {$table} WHERE id = %d",
|
|
$shortcode_id
|
|
)
|
|
);
|
|
|
|
if (!$shortcode) {
|
|
wp_send_json_error(['message' => 'Shortcode not found']);
|
|
}
|
|
|
|
wp_send_json_success($shortcode);
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for saving shortcode
|
|
*/
|
|
public function ajax_save_shortcode() {
|
|
check_ajax_referer('bd_bookshelf_admin_nonce', 'shortcode_nonce');
|
|
|
|
if (!current_user_can('manage_options')) {
|
|
wp_send_json_error(['message' => 'Insufficient permissions']);
|
|
}
|
|
|
|
$shortcode_id = absint($_POST['shortcode_id'] ?? 0);
|
|
|
|
$data = [
|
|
'slug' => sanitize_title($_POST['slug'] ?? ''),
|
|
'target_url' => esc_url_raw($_POST['target_url'] ?? ''),
|
|
'redirect_type' => sanitize_text_field($_POST['redirect_type'] ?? '301'),
|
|
'description' => sanitize_text_field($_POST['description'] ?? ''),
|
|
];
|
|
|
|
if (empty($data['slug']) || empty($data['target_url'])) {
|
|
wp_send_json_error(['message' => 'Slug and Target URL are required']);
|
|
}
|
|
|
|
if ($shortcode_id) {
|
|
$result = Umbra_Shortcodes::update_shortcode($shortcode_id, $data);
|
|
} else {
|
|
$shortcode_id = Umbra_Shortcodes::create_shortcode($data['slug'], $data['target_url'], $data['redirect_type'], $data['description']);
|
|
$result = $shortcode_id !== false;
|
|
}
|
|
|
|
if ($result) {
|
|
wp_send_json_success(['message' => 'Shortcode saved', 'id' => $shortcode_id]);
|
|
} else {
|
|
wp_send_json_error(['message' => 'Failed to save shortcode']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for deleting shortcode
|
|
*/
|
|
public function ajax_delete_shortcode() {
|
|
check_ajax_referer('bd_bookshelf_admin_nonce', 'nonce');
|
|
|
|
if (!current_user_can('manage_options')) {
|
|
wp_send_json_error(['message' => 'Insufficient permissions']);
|
|
}
|
|
|
|
$shortcode_id = absint($_POST['id'] ?? 0);
|
|
if (!$shortcode_id) {
|
|
wp_send_json_error(['message' => 'Invalid shortcode ID']);
|
|
}
|
|
|
|
$result = Umbra_Shortcodes::delete_shortcode($shortcode_id);
|
|
|
|
if ($result) {
|
|
wp_send_json_success(['message' => 'Shortcode deleted']);
|
|
} else {
|
|
wp_send_json_error(['message' => 'Failed to delete shortcode']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for saving sites/affiliate codes
|
|
*/
|
|
public function ajax_save_sites() {
|
|
check_ajax_referer('bd_bookshelf_admin_nonce', 'sites_nonce');
|
|
|
|
if (!current_user_can('manage_options')) {
|
|
wp_send_json_error(['message' => 'Insufficient permissions']);
|
|
}
|
|
|
|
if (!empty($_POST['affiliate'])) {
|
|
global $wpdb;
|
|
$table = Umbra_Database::table('retail_sites');
|
|
|
|
foreach ($_POST['affiliate'] as $site_id => $code) {
|
|
$wpdb->update(
|
|
$table,
|
|
['affiliate_code' => sanitize_text_field($code)],
|
|
['id' => absint($site_id)],
|
|
['%s'],
|
|
['%d']
|
|
);
|
|
}
|
|
}
|
|
|
|
wp_send_json_success(['message' => 'Affiliate codes saved']);
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for saving settings
|
|
*/
|
|
public function ajax_save_settings() {
|
|
check_ajax_referer('bd_bookshelf_save_settings', 'nonce');
|
|
|
|
if (!current_user_can('manage_options')) {
|
|
wp_send_json_error(['message' => 'Insufficient permissions']);
|
|
}
|
|
|
|
Umbra_GDPR::update_consent_text($_POST['gdpr_text'] ?? '');
|
|
|
|
wp_send_json_success(['message' => 'Settings saved']);
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for CSV import
|
|
*/
|
|
public function ajax_import_csv() {
|
|
check_ajax_referer('bd_bookshelf_admin_nonce', 'import_nonce');
|
|
|
|
if (!current_user_can('manage_options')) {
|
|
wp_send_json_error(['message' => 'Insufficient permissions']);
|
|
}
|
|
|
|
if (empty($_FILES['csv_file']['tmp_name'])) {
|
|
wp_send_json_error(['message' => 'No file uploaded']);
|
|
}
|
|
|
|
$file = $_FILES['csv_file']['tmp_name'];
|
|
$handle = fopen($file, 'r');
|
|
|
|
if (!$handle) {
|
|
wp_send_json_error(['message' => 'Unable to open file']);
|
|
}
|
|
|
|
// Get header row
|
|
$header = fgetcsv($handle);
|
|
|
|
$imported = 0;
|
|
$updated = 0;
|
|
$skipped = 0;
|
|
$errors = [];
|
|
|
|
while (($row = fgetcsv($handle)) !== false) {
|
|
$data = array_combine($header, $row);
|
|
|
|
if (empty($data['title'])) {
|
|
$errors[] = 'Row ' . ($imported + $skipped + 1) . ': Missing title';
|
|
continue;
|
|
}
|
|
|
|
// Check if already imported - update instead of skip
|
|
$existing = Umbra_Book_Manager::get_book_by_title($data['title']);
|
|
|
|
$book_data = [
|
|
'title' => $data['title'],
|
|
'slug' => sanitize_title($data['title']),
|
|
'description' => $data['description'] ?? '',
|
|
'isbn_ebook' => $data['isbn_ebook'] ?? '',
|
|
'isbn_paperback' => $data['isbn_paperback'] ?? '',
|
|
'isbn_hardcover' => $data['isbn_hardcover'] ?? '',
|
|
'isbn_audio' => $data['isbn_audio'] ?? '',
|
|
'amazon_asin' => $data['amazon_asin'] ?? '',
|
|
'goodreads_id' => $data['goodreads_id'] ?? '',
|
|
];
|
|
|
|
if ($existing) {
|
|
// Update existing book - fill in empty fields
|
|
$update_data = [];
|
|
foreach ($book_data as $key => $value) {
|
|
if ($key === 'title' || $key === 'slug') continue;
|
|
if (!empty($value) && empty($existing->$key)) {
|
|
$update_data[$key] = $value;
|
|
}
|
|
}
|
|
if (!empty($update_data)) {
|
|
Umbra_Book_Manager::update_book($existing->id, $update_data);
|
|
$updated++;
|
|
} else {
|
|
$skipped++;
|
|
}
|
|
} else {
|
|
// Create new book
|
|
$book_id = Umbra_Book_Manager::create_book($book_data);
|
|
if ($book_id) {
|
|
$imported++;
|
|
} else {
|
|
$errors[] = 'Row ' . ($imported + $skipped + 1) . ': Failed to create book';
|
|
}
|
|
}
|
|
}
|
|
|
|
fclose($handle);
|
|
|
|
$message = sprintf('Imported %d new books, updated %d existing, skipped %d', $imported, $updated, $skipped);
|
|
if (!empty($errors)) {
|
|
$message .= '. ' . count($errors) . ' errors.';
|
|
}
|
|
|
|
wp_send_json_success([
|
|
'message' => $message,
|
|
'imported' => $imported,
|
|
'updated' => $updated,
|
|
'skipped' => $skipped,
|
|
'errors' => $errors,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for posts import
|
|
*/
|
|
public function ajax_import_posts() {
|
|
check_ajax_referer('bd_bookshelf_admin_nonce', 'import_posts_nonce');
|
|
|
|
if (!current_user_can('manage_options')) {
|
|
wp_send_json_error(['message' => 'Insufficient permissions']);
|
|
}
|
|
|
|
$post_type = sanitize_text_field($_POST['post_type'] ?? 'post');
|
|
$limit = absint($_POST['limit'] ?? 100);
|
|
|
|
$posts = get_posts([
|
|
'post_type' => $post_type,
|
|
'post_status' => 'publish',
|
|
'numberposts' => $limit,
|
|
]);
|
|
|
|
$imported = 0;
|
|
$skipped = 0;
|
|
|
|
foreach ($posts as $post) {
|
|
// Check if already imported
|
|
$existing = Umbra_Book_Manager::get_book_by_title($post->post_title);
|
|
if ($existing) {
|
|
$skipped++;
|
|
continue;
|
|
}
|
|
|
|
// Import book
|
|
$book_id = Umbra_Book_Manager::create_book([
|
|
'title' => $post->post_title,
|
|
'description' => $post->post_content,
|
|
]);
|
|
|
|
if ($book_id) {
|
|
// Link to WordPress post
|
|
update_post_meta($post->ID, '_bd_bookshelf_book_id', $book_id);
|
|
$imported++;
|
|
}
|
|
}
|
|
|
|
wp_send_json_success([
|
|
'message' => sprintf('Imported %d books, skipped %d', $imported, $skipped),
|
|
'imported' => $imported,
|
|
'skipped' => $skipped,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* AJAX handler for manual sync
|
|
*/
|
|
public function ajax_sync_ratings() {
|
|
check_ajax_referer('bd_bookshelf_admin_nonce', 'nonce');
|
|
|
|
if (!current_user_can('manage_options')) {
|
|
wp_send_json_error(['message' => 'Insufficient permissions']);
|
|
}
|
|
|
|
$result = Umbra_Rating_Sync::manual_sync();
|
|
|
|
wp_send_json_success([
|
|
'message' => sprintf('Synced %d books, failed %d', $result['synced'], $result['failed']),
|
|
'result' => $result,
|
|
]);
|
|
}
|
|
}
|