Consolidate three plugins into Umbra Bookshelf

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.
This commit is contained in:
bdm
2026-10-18 19:55:36 -07:00
parent 71e691d459
commit dec732931c
54 changed files with 2091 additions and 1263 deletions

View File

@@ -1,158 +0,0 @@
<?php
// Copyright 2026 Umbra Tools (umbratools.com)
// SPDX-License-Identifier: Apache-2.0
/**
* Plugin Name: BD Bookshelf
* Plugin URI: https://authorbdmurphy.com/plugins/bd-bookshelf
* Description: Book management plugin for authors with ISBN tracking, retail links, rating synchronization, and series management.
* Version: 0.1.0
* Author: B.D. Murphy
* Author URI: https://authorbdmurphy.com
* License: GPLv2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: bd-bookshelf
* Domain Path: /languages
* Requires at least: 6.0
* Requires PHP: 7.4
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Plugin constants
*/
define('BD_BOOKSHELF_VERSION', '0.1.0');
define('BD_BOOKSHELF_DIR', plugin_dir_path(__FILE__));
define('BD_BOOKSHELF_URL', plugin_dir_url(__FILE__));
define('BD_BOOKSHELF_BASENAME', plugin_basename(__FILE__));
/**
* Include files
*/
require_once BD_BOOKSHELF_DIR . 'includes/class-bdm-database.php';
require_once BD_BOOKSHELF_DIR . 'includes/class-bdm-book-manager.php';
require_once BD_BOOKSHELF_DIR . 'includes/class-bdm-authors.php';
require_once BD_BOOKSHELF_DIR . 'includes/class-bdm-series.php';
require_once BD_BOOKSHELF_DIR . 'includes/class-bdm-retail-sites.php';
require_once BD_BOOKSHELF_DIR . 'includes/class-bdm-rating-sync.php';
require_once BD_BOOKSHELF_DIR . 'includes/class-bdm-shortcodes.php';
require_once BD_BOOKSHELF_DIR . 'includes/class-bdm-seo.php';
require_once BD_BOOKSHELF_DIR . 'includes/class-bdm-gdpr.php';
require_once BD_BOOKSHELF_DIR . 'includes/class-bdm-goodreads-scraper.php';
require_once BD_BOOKSHELF_DIR . 'includes/class-bdm-amazon-scraper.php';
require_once BD_BOOKSHELF_DIR . 'includes/class-bdm-admin.php';
require_once BD_BOOKSHELF_DIR . 'includes/class-bdm-migration.php';
require_once BD_BOOKSHELF_DIR . 'includes/class-bdm-theme-compatibility.php';
/**
* Main plugin class
*/
final class BD_Bookshelf {
/**
* Single instance of the class
*/
private static $instance = null;
/**
* Get single instance
*/
public static function instance() {
if (is_null(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
$this->init_hooks();
}
/**
* Initialize hooks
*/
private function init_hooks() {
register_activation_hook(__FILE__, [$this, 'activate']);
register_deactivation_hook(__FILE__, [$this, 'deactivate']);
add_action('init', [$this, 'init']);
add_action('plugins_loaded', [$this, 'plugins_loaded']);
// Initialize components
add_action('init', [$this, 'init_components']);
}
/**
* Plugin activation
*/
public function activate() {
$database = new BDM_Database();
$database->create_tables();
// Flush rewrite rules
flush_rewrite_rules();
// Store activation time
update_option('bd_bookshelf_activation_time', current_time('timestamp'));
}
/**
* Plugin deactivation
*/
public function deactivate() {
// Clear scheduled events
wp_clear_scheduled_hook('bd_bookshelf_sync_ratings');
// Flush rewrite rules
flush_rewrite_rules();
}
/**
* Init hook
*/
public function init() {
// Load text domain
load_plugin_textdomain('bd-bookshelf', false, dirname(BD_BOOKSHELF_BASENAME) . '/languages');
}
/**
* Plugins loaded hook
*/
public function plugins_loaded() {
// Check for updates, etc.
}
/**
* Initialize components
*/
public function init_components() {
// Initialize admin
if (is_admin()) {
new BDM_Admin();
new BDM_Migration();
}
// Initialize public facing features
new BDM_Shortcodes();
new BDM_GDPR();
// Initialize rating sync (cron)
new BDM_Rating_Sync();
}
}
/**
* Initialize plugin
*/
function bd_bookshelf() {
return BD_Bookshelf::instance();
}
// Start the plugin
bd_bookshelf();

View File

@@ -1,77 +0,0 @@
=== BD Bookshelf ===
Contributors: bdmurphy
Requires at least: 6.0
Tested up to: 6.5
Requires PHP: 7.4
Stable tag: 0.1.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Copyright 2026 B.D. Murphy
Book management plugin for authors with ISBN tracking, retail links, and rating synchronization.
== Description ==
BD Bookshelf is a comprehensive book management plugin designed for authors. It provides:
* **Book Management** - Custom database tables for efficient book storage
* **ISBN Tracking** - Support for 4 ISBN formats (eBook, Paperback, Hard Cover, Audio)
* **Retail Links** - Automatic link generation from ISBNs for Amazon, B&N, Apple Books, Google Play, and Kobo
* **Rating Synchronization** - Automated syncing of ratings from Amazon and Goodreads via web scraping
* **Shortcodes** - Display retail grids and ratings anywhere on your site
* **Series Management** - Organize books into series with ordering
* **Author Management** - Support for up to 3 authors per book
* **SEO Metadata** - Custom meta tags and Open Graph data for books
* **GDPR Compliance** - Cookie consent banner for affiliate tracking
* **Click Analytics** - Track clicks on custom redirect shortcodes
* **Data Migration** - Import existing books from WordPress posts or CSV files
== Installation ==
1. Upload the `bd-bookshelf` folder to the `/wp-content/plugins/` directory
2. Activate the plugin through the 'Plugins' menu in WordPress
3. Navigate to the 'Books' menu to start adding books
4. Configure affiliate codes in Settings > Books > Settings
== Frequently Asked Questions ==
= How do I add a new book? =
Go to Books > Add New Book. Fill in the title, description, ISBNs, and other details. Retail links are automatically generated when you enter ISBNs.
= How do I display retail links? =
Use the shortcodes `[sell_ebook]`, `[sell_pb]`, `[sell_hc]`, or `[sell_audio]` with the book ID:
`[sell_ebook id="123"]`
= How do I display ratings? =
Use the `[rating]` shortcode:
`[rating title="My Book" format="both"]`
= How often are ratings synced? =
Ratings are automatically synced every 12 hours. You can also trigger a manual sync from the Rating Sync page.
== Changelog ==
= 0.1.0 =
* Initial release
* Book management with custom database tables
* ISBN tracking for 4 formats
* Automatic retail link generation
* Rating synchronization from Amazon and Goodreads
* Shortcode suite for retail grids and ratings
* Series and author management
* SEO metadata
* GDPR compliance
* Click analytics
* Data migration tools
== Upgrade Notice ==
= 0.1.0 =
Initial release of BD Bookshelf plugin.

View File

@@ -1,30 +0,0 @@
<?php
// Copyright 2026 Umbra Tools (umbratools.com)
// SPDX-License-Identifier: Apache-2.0
/**
* BD Bookshelf Uninstall
*
* Fired when the plugin is deleted from the Plugins page.
*
* @package BD_Bookshelf
*/
if (!defined('WP_UNINSTALL_PLUGIN')) {
exit;
}
// Include database class
require_once __DIR__ . '/includes/class-bdm-database.php';
// Drop all tables
$database = new BDM_Database();
$database->drop_tables();
// Remove options
global $wpdb;
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'bd_bookshelf_%'");
// Remove transients
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_bd_bookshelf_%'");
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_bd_bookshelf_%'");

View File

@@ -1,61 +0,0 @@
<?php
// Copyright 2026 Umbra Tools (umbratools.com)
// SPDX-License-Identifier: Apache-2.0
/**
* Plugin Name: BD BookShelfHelper
* Plugin URI: https://authorbdmurphy.com/plugins/bd-bookshelfhelper
* Description: Manages book data, series, pen names, short links, reviews, and ratings for the BD Murphy Bookshelf theme.
* Version: 2.0.0
* Author: B.D. Murphy
* Author URI: https://authorbdmurphy.com
* License: GPLv2 or later
* Text Domain: bd-bookshelfhelper
* Requires at least: 6.0
* Requires PHP: 7.4
*/
if (!defined('ABSPATH')) {
exit;
}
define('BD_BHS_VERSION', '2.0.0');
define('BD_BHS_DIR', plugin_dir_path(__FILE__));
define('BD_BHS_URI', plugin_dir_url(__FILE__));
// Main plugin class
require_once BD_BHS_DIR . 'inc/class-bookhelper.php';
require_once BD_BHS_DIR . 'inc/class-admin.php';
require_once BD_BHS_DIR . 'inc/class-install.php';
/**
* Get plugin instance
*
* @return BD_BookShelfHelper
*/
function BD_BookShelfHelper() {
return BD_BookShelfHelper::instance();
}
/**
* Initialize plugin
*/
function bd_bookshelf_init() {
// Initialize main plugin
BD_BookShelfHelper::instance();
// Initialize admin (only in admin)
if (is_admin()) {
BD_BookShelfHelper_Admin::instance();
}
}
add_action('plugins_loaded', 'bd_bookshelf_init', 10);
// Activation/Deactivation hooks
register_activation_hook(__FILE__, array('BD_BookShelfHelper_Install', 'activate'));
register_deactivation_hook(__FILE__, array('BD_BookShelfHelper_Install', 'deactivate'));
// Load plugin text domain
add_action('plugins_loaded', function() {
load_plugin_textdomain('bd-bookshelfhelper', false, dirname(plugin_basename(__FILE__)) . '/languages');
});

View File

@@ -1,81 +0,0 @@
# BD BookShelfHelper
**Contributors:** B.D. Murphy
**Tags:** books, author, series, pen names, reviews
**Requires at least:** WordPress 6.0
**Tested up to:** WordPress 6.4
**Requires PHP:** 7.4
**License:** GPL v2 or later
**License URI:** https://www.gnu.org/licenses/gpl-2.0.html
Manages book data, series, pen names, short links, reviews, and ratings for the BD Murphy Bookshelf theme.
## Description
BD BookShelfHelper is a WordPress plugin designed to work with the BDMurphy Bookshelf theme. It provides comprehensive management for:
- **Book Management:** Full custom post type for books with multi-language support
- **Series System:** Organize books into series with volume numbers
- **Pen Names:** Support for multiple author pen names
- **Short Links:** Auto-generated short URLs for books (e.g., /ph/ for "Pandemic Hacker")
- **Review Short Links:** Dedicated URLs for reader reviews on each retailer
- **Ratings:** Amazon and Goodreads rating integration
- **SEO:** Auto-generated meta tags and schema.org Book structured data
## Installation
1. Upload the `bd-bookshelfhelper` folder to the `/wp-content/plugins/` directory
2. Activate the plugin through the 'Plugins' menu in WordPress
3. Configure settings under 'BD BookHelper' in the admin sidebar
## Configuration
### Setting Up Books
1. Go to BD BookHelper > Add Book
2. Fill in book details including ASIN/ISBN
3. Add localized content for each supported language
4. Set retailer purchase links
5. Add reviews and ratings
### Pen Names
1. Go to BD BookHelper > Pen Names
2. Add author pen names
3. Assign to books in the book editor
### Series
1. Go to BD BookHelper > Series (taxonomy)
2. Create series with name, slug, and description
3. Assign books to series with volume numbers
### Settings
Configure affiliate codes and integrations under BD BookHelper > Settings.
## Short Links
Books automatically get short URLs based on their title:
- "Pandemic Hacker" → /ph/
- "Deal Hunter" → /dh/
Review short links follow the pattern: `{book-slug}r{site-code}`:
- /phrA = Amazon review
- /phrG = Goodreads review
## Frequently Asked Questions
### Do I need the theme?
Yes, this plugin is designed to work with the BDMurphy Bookshelf theme.
### Can I use without affiliate codes?
Yes, the plugin works without affiliate codes. They are optional.
### What languages are supported?
English, French, Spanish, German, Italian, Portuguese, Russian, Japanese, and Chinese (Simplified).
## Changelog
### 2.0.0
- Initial release with full book management, series, pen names, and integrations

View File

@@ -1,39 +0,0 @@
<?php
// Copyright 2026 Umbra Tools (umbratools.com)
// SPDX-License-Identifier: Apache-2.0
/**
* Uninstall BD BookShelfHelper
*
* Removes all plugin data when the plugin is deleted.
*/
if (!defined('WP_UNINSTALL_PLUGIN')) {
exit;
}
// Drop database tables
global $wpdb;
$tables = array(
$wpdb->prefix . 'bd_books',
$wpdb->prefix . 'bd_book_meta',
$wpdb->prefix . 'bd_series',
$wpdb->prefix . 'bd_pen_names',
$wpdb->prefix . 'bd_reviews',
$wpdb->prefix . 'bd_ratings',
$wpdb->prefix . 'bd_review_shortlinks',
);
foreach ($tables as $table) {
$wpdb->query("DROP TABLE IF EXISTS {$table}");
}
// Delete options
delete_option('bdm_affiliate_codes');
delete_option('bdm_goodreads_author_id');
delete_option('bdm_awards');
// Clear scheduled events
wp_clear_scheduled_hook('bd_bookshelf_goodreads_refresh');
wp_clear_scheduled_hook('bd_bookshelf_amazon_scrape');

View File

@@ -1,52 +0,0 @@
<?php
// Copyright 2026 Umbra Tools (umbratools.com)
// SPDX-License-Identifier: Apache-2.0
/**
* Plugin Name: BD SimpleEmail
* Plugin URI: https://authorbdmurphy.com/plugins/bd-simpleemail
* Description: Email newsletter management with AI-assisted composition using Gemini API and subscriber management.
* Version: 2.0.0
* Author: B.D. Murphy
* Author URI: https://authorbdmurphy.com
* License: GPLv2 or later
* Text Domain: bd-simpleemail
* Requires at least: 6.0
* Requires PHP: 7.4
*/
if (!defined('ABSPATH')) {
exit;
}
define('BD_EMAIL_VERSION', '2.0.0');
define('BD_EMAIL_DIR', plugin_dir_path(__FILE__));
define('BD_EMAIL_URI', plugin_dir_url(__FILE__));
require_once BD_EMAIL_DIR . 'inc/class-simpleemail.php';
require_once BD_EMAIL_DIR . 'inc/class-email-admin.php';
require_once BD_EMAIL_DIR . 'inc/class-email-install.php';
require_once BD_EMAIL_DIR . 'inc/class-email-sender.php';
/**
* Get plugin instance
*
* @return BD_SimpleEmail
*/
function BD_SimpleEmail() {
return BD_SimpleEmail::instance();
}
/**
* Initialize plugin
*/
add_action('plugins_loaded', 'BD_SimpleEmail');
// Activation/Deactivation hooks
register_activation_hook(__FILE__, array('BD_SimpleEmail_Install', 'activate'));
register_deactivation_hook(__FILE__, array('BD_SimpleEmail_Install', 'deactivate'));
// Load plugin text domain
add_action('plugins_loaded', function() {
load_plugin_textdomain('bd-simpleemail', false, dirname(plugin_basename(__FILE__)) . '/languages');
});

View File

@@ -1,73 +0,0 @@
# BD SimpleEmail
**Contributors:** B.D. Murphy
**Tags:** email, newsletter, subscribers, mailing list
**Requires at least:** WordPress 6.0
**Tested up to:** WordPress 6.4
**Requires PHP:** 7.4
**License:** GPL v2 or later
**License URI:** https://www.gnu.org/licenses/gpl-2.0.html
Email newsletter management with AI-assisted composition using Gemini API and subscriber management.
## Description
BD SimpleEmail provides comprehensive email newsletter capabilities:
- **Subscriber Management:** Add, remove, and manage email subscribers
- **AI-Assisted Writing:** Generate newsletter drafts using Gemini AI
- **Gmail SMTP:** Send newsletters via your Gmail account
- **One-Click Unsubscribe:** Compliant with Gmail/Yahoo requirements
- **CAN-SPAM Compliance:** Automatic business information in email footers
- **Shortcode Support:** `[bdm_signup]` for embeddable signup forms
## Installation
1. Upload the `bd-simpleemail` folder to the `/wp-content/plugins/` directory
2. Activate the plugin through the 'Plugins' menu in WordPress
3. Configure Gmail settings under BD SimpleEmail > Settings
## Configuration
### Gmail Setup
1. Enable 2-Factor Authentication on your Google account
2. Generate an App Password: https://support.google.com/accounts/answer/185833
3. Enter your Gmail address and App Password in plugin settings
### Gemini AI (Optional)
1. Get an API key: https://makersuite.google.com/app/apikey
2. Enter the API key in plugin settings
### Business Information
Configure your business address in Appearance > Theme Options (required for CAN-SPAM compliance).
## Sending Newsletters
1. Go to BD SimpleEmail > New Newsletter
2. Enter a subject line
3. (Optional) Use AI to generate content
4. Write or edit your newsletter
5. Click "Send Newsletter"
## Shortcodes
Use `[bdm_signup]` to display a signup form anywhere on your site.
## Frequently Asked Questions
### Do I need to use Gmail?
Yes, this plugin uses Gmail SMTP for sending. App passwords are required.
### Is AI required?
No, AI assistance is optional. You can write newsletters manually.
### Is one-click unsubscribe included?
Yes, all emails include one-click unsubscribe links compliant with Gmail/Yahoo requirements.
## Changelog
### 2.0.0
- Initial release with subscriber management, AI composition, and Gmail SMTP sending

View File

@@ -1,32 +0,0 @@
<?php
// Copyright 2026 Umbra Tools (umbratools.com)
// SPDX-License-Identifier: Apache-2.0
/**
* Uninstall BD SimpleEmail
*
* Removes all plugin data when the plugin is deleted.
*/
if (!defined('WP_UNINSTALL_PLUGIN')) {
exit;
}
// Drop database tables
global $wpdb;
$tables = array(
$wpdb->prefix . 'bd_subscribers',
$wpdb->prefix . 'bd_newsletters',
);
foreach ($tables as $table) {
$wpdb->query("DROP TABLE IF EXISTS {$table}");
}
// Delete options
delete_option('bdm_gmail_address');
delete_option('bdm_gmail_password');
delete_option('bdm_sender_name');
delete_option('bdm_reply_to');
delete_option('bdm_gemini_api_key');

View File

@@ -1,7 +1,7 @@
/** /**
* BD Bookshelf - Admin Styles * BD Bookshelf - Admin Styles
* *
* @package BD_Bookshelf * @package Umbra_Bookshelf
*/ */
/* Form Layout */ /* Form Layout */

View File

@@ -4,7 +4,7 @@
/** /**
* BD Bookshelf - Admin JavaScript * BD Bookshelf - Admin JavaScript
* *
* @package BD_Bookshelf * @package Umbra_Bookshelf
*/ */
(function($) { (function($) {
@@ -71,11 +71,11 @@
btn.prop('disabled', true).text('Syncing...'); btn.prop('disabled', true).text('Syncing...');
$.ajax({ $.ajax({
url: bdBookshelfAdmin.ajaxurl, url: umbraBookshelfAdmin.ajaxurl,
type: 'POST', type: 'POST',
data: { data: {
action: 'bd_bookshelf_sync_ratings', action: 'bd_bookshelf_sync_ratings',
nonce: bdBookshelfAdmin.nonce nonce: umbraBookshelfAdmin.nonce
}, },
success: function(response) { success: function(response) {
if (response.success) { if (response.success) {
@@ -98,12 +98,12 @@
btn.prop('disabled', true).text('Regenerating...'); btn.prop('disabled', true).text('Regenerating...');
$.ajax({ $.ajax({
url: bdBookshelfAdmin.ajaxurl, url: umbraBookshelfAdmin.ajaxurl,
type: 'POST', type: 'POST',
data: { data: {
action: 'bd_bookshelf_regenerate_links', action: 'bd_bookshelf_regenerate_links',
book_id: bookId, book_id: bookId,
nonce: bdBookshelfAdmin.nonce nonce: umbraBookshelfAdmin.nonce
}, },
success: function(response) { success: function(response) {
if (response.success) { if (response.success) {
@@ -170,12 +170,12 @@
$('body').css('cursor', 'wait'); $('body').css('cursor', 'wait');
$.ajax({ $.ajax({
url: bdBookshelfAdmin.ajaxurl, url: umbraBookshelfAdmin.ajaxurl,
type: 'POST', type: 'POST',
data: { data: {
action: action, action: action,
author_id: authorId, author_id: authorId,
nonce: bdBookshelfAdmin.nonce nonce: umbraBookshelfAdmin.nonce
}, },
success: function(response) { success: function(response) {
$('body').css('cursor', 'default'); $('body').css('cursor', 'default');
@@ -300,7 +300,7 @@
// Build form data manually for proper nested array handling // Build form data manually for proper nested array handling
var formData = new FormData(); var formData = new FormData();
formData.append('action', 'bd_bookshelf_import_scraped'); formData.append('action', 'bd_bookshelf_import_scraped');
formData.append('nonce', bdBookshelfAdmin.nonce); formData.append('nonce', umbraBookshelfAdmin.nonce);
formData.append('source', source); formData.append('source', source);
// Add author data // Add author data
@@ -326,7 +326,7 @@
}); });
$.ajax({ $.ajax({
url: bdBookshelfAdmin.ajaxurl, url: umbraBookshelfAdmin.ajaxurl,
type: 'POST', type: 'POST',
data: formData, data: formData,
processData: false, processData: false,
@@ -365,8 +365,8 @@
} }
coverFrame = wp.media({ coverFrame = wp.media({
title: bdBookshelfAdmin.mediaTitle, title: umbraBookshelfAdmin.mediaTitle,
button: { text: bdBookshelfAdmin.mediaButton }, button: { text: umbraBookshelfAdmin.mediaButton },
multiple: false, multiple: false,
library: { type: 'image' } library: { type: 'image' }
}); });
@@ -458,12 +458,12 @@
// Fetch author data // Fetch author data
$.ajax({ $.ajax({
url: bdBookshelfAdmin.ajaxurl, url: umbraBookshelfAdmin.ajaxurl,
type: 'POST', type: 'POST',
data: { data: {
action: 'bd_bookshelf_get_author', action: 'bd_bookshelf_get_author',
id: authorId, id: authorId,
nonce: bdBookshelfAdmin.nonce nonce: umbraBookshelfAdmin.nonce
}, },
success: function(response) { success: function(response) {
if (response.success) { if (response.success) {
@@ -497,12 +497,12 @@
var authorId = $(this).data('id'); var authorId = $(this).data('id');
if (confirm('Are you sure you want to delete this author?')) { if (confirm('Are you sure you want to delete this author?')) {
$.ajax({ $.ajax({
url: bdBookshelfAdmin.ajaxurl, url: umbraBookshelfAdmin.ajaxurl,
type: 'POST', type: 'POST',
data: { data: {
action: 'bd_bookshelf_delete_author', action: 'bd_bookshelf_delete_author',
id: authorId, id: authorId,
nonce: bdBookshelfAdmin.nonce nonce: umbraBookshelfAdmin.nonce
}, },
success: function(response) { success: function(response) {
if (response.success) { if (response.success) {
@@ -521,7 +521,7 @@
var data = $(this).serialize(); var data = $(this).serialize();
$.ajax({ $.ajax({
url: bdBookshelfAdmin.ajaxurl, url: umbraBookshelfAdmin.ajaxurl,
type: 'POST', type: 'POST',
data: data + '&action=bd_bookshelf_save_author', data: data + '&action=bd_bookshelf_save_author',
success: function(response) { success: function(response) {
@@ -560,12 +560,12 @@
// Fetch series data // Fetch series data
$.ajax({ $.ajax({
url: bdBookshelfAdmin.ajaxurl, url: umbraBookshelfAdmin.ajaxurl,
type: 'POST', type: 'POST',
data: { data: {
action: 'bd_bookshelf_get_series', action: 'bd_bookshelf_get_series',
id: seriesId, id: seriesId,
nonce: bdBookshelfAdmin.nonce nonce: umbraBookshelfAdmin.nonce
}, },
success: function(response) { success: function(response) {
if (response.success) { if (response.success) {
@@ -586,12 +586,12 @@
var seriesId = $(this).data('id'); var seriesId = $(this).data('id');
if (confirm('Are you sure you want to delete this series? Books in this series will be unlinked.')) { if (confirm('Are you sure you want to delete this series? Books in this series will be unlinked.')) {
$.ajax({ $.ajax({
url: bdBookshelfAdmin.ajaxurl, url: umbraBookshelfAdmin.ajaxurl,
type: 'POST', type: 'POST',
data: { data: {
action: 'bd_bookshelf_delete_series', action: 'bd_bookshelf_delete_series',
id: seriesId, id: seriesId,
nonce: bdBookshelfAdmin.nonce nonce: umbraBookshelfAdmin.nonce
}, },
success: function(response) { success: function(response) {
if (response.success) { if (response.success) {
@@ -610,7 +610,7 @@
var data = $(this).serialize(); var data = $(this).serialize();
$.ajax({ $.ajax({
url: bdBookshelfAdmin.ajaxurl, url: umbraBookshelfAdmin.ajaxurl,
type: 'POST', type: 'POST',
data: data + '&action=bd_bookshelf_save_series', data: data + '&action=bd_bookshelf_save_series',
success: function(response) { success: function(response) {
@@ -646,12 +646,12 @@
var shortcodeId = $(this).data('id'); var shortcodeId = $(this).data('id');
$.ajax({ $.ajax({
url: bdBookshelfAdmin.ajaxurl, url: umbraBookshelfAdmin.ajaxurl,
type: 'POST', type: 'POST',
data: { data: {
action: 'bd_bookshelf_get_shortcode', action: 'bd_bookshelf_get_shortcode',
id: shortcodeId, id: shortcodeId,
nonce: bdBookshelfAdmin.nonce nonce: umbraBookshelfAdmin.nonce
}, },
success: function(response) { success: function(response) {
if (response.success) { if (response.success) {
@@ -674,12 +674,12 @@
var shortcodeId = $(this).data('id'); var shortcodeId = $(this).data('id');
if (confirm('Are you sure you want to delete this shortcode?')) { if (confirm('Are you sure you want to delete this shortcode?')) {
$.ajax({ $.ajax({
url: bdBookshelfAdmin.ajaxurl, url: umbraBookshelfAdmin.ajaxurl,
type: 'POST', type: 'POST',
data: { data: {
action: 'bd_bookshelf_delete_shortcode', action: 'bd_bookshelf_delete_shortcode',
id: shortcodeId, id: shortcodeId,
nonce: bdBookshelfAdmin.nonce nonce: umbraBookshelfAdmin.nonce
}, },
success: function(response) { success: function(response) {
if (response.success) { if (response.success) {
@@ -698,7 +698,7 @@
var data = $(this).serialize(); var data = $(this).serialize();
$.ajax({ $.ajax({
url: bdBookshelfAdmin.ajaxurl, url: umbraBookshelfAdmin.ajaxurl,
type: 'POST', type: 'POST',
data: data + '&action=bd_bookshelf_save_shortcode', data: data + '&action=bd_bookshelf_save_shortcode',
success: function(response) { success: function(response) {
@@ -720,7 +720,7 @@
var data = form.serialize(); var data = form.serialize();
$.ajax({ $.ajax({
url: bdBookshelfAdmin.ajaxurl, url: umbraBookshelfAdmin.ajaxurl,
type: 'POST', type: 'POST',
data: data + '&action=bd_bookshelf_save_book', data: data + '&action=bd_bookshelf_save_book',
success: function(response) { success: function(response) {
@@ -741,12 +741,12 @@
*/ */
function deleteBook(bookId) { function deleteBook(bookId) {
$.ajax({ $.ajax({
url: bdBookshelfAdmin.ajaxurl, url: umbraBookshelfAdmin.ajaxurl,
type: 'POST', type: 'POST',
data: { data: {
action: 'bd_bookshelf_delete_book', action: 'bd_bookshelf_delete_book',
id: bookId, id: bookId,
nonce: bdBookshelfAdmin.nonce nonce: umbraBookshelfAdmin.nonce
}, },
success: function(response) { success: function(response) {
if (response.success) { if (response.success) {
@@ -766,7 +766,7 @@
var data = form.serialize(); var data = form.serialize();
$.ajax({ $.ajax({
url: bdBookshelfAdmin.ajaxurl, url: umbraBookshelfAdmin.ajaxurl,
type: 'POST', type: 'POST',
data: data + '&action=bd_bookshelf_save_sites', data: data + '&action=bd_bookshelf_save_sites',
success: function(response) { success: function(response) {
@@ -787,7 +787,7 @@
var data = form.serialize(); var data = form.serialize();
$.ajax({ $.ajax({
url: bdBookshelfAdmin.ajaxurl, url: umbraBookshelfAdmin.ajaxurl,
type: 'POST', type: 'POST',
data: data + '&action=bd_bookshelf_save_settings', data: data + '&action=bd_bookshelf_save_settings',
success: function(response) { success: function(response) {
@@ -809,7 +809,7 @@
formData.append('action', 'bd_bookshelf_import_csv'); formData.append('action', 'bd_bookshelf_import_csv');
$.ajax({ $.ajax({
url: bdBookshelfAdmin.ajaxurl, url: umbraBookshelfAdmin.ajaxurl,
type: 'POST', type: 'POST',
data: formData, data: formData,
processData: false, processData: false,
@@ -833,7 +833,7 @@
var data = form.serialize(); var data = form.serialize();
$.ajax({ $.ajax({
url: bdBookshelfAdmin.ajaxurl, url: umbraBookshelfAdmin.ajaxurl,
type: 'POST', type: 'POST',
data: data + '&action=bd_bookshelf_import_posts', data: data + '&action=bd_bookshelf_import_posts',
success: function(response) { success: function(response) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,70 @@
# Architecture
## Why one plugin
Three plugins shipped the same product surface under three admin menus and
shared two cron hooks. Users installed, updated and deactivated them
independently while the code assumed all three were present. Consolidating into
one plugin makes the dependency explicit and lets one activation hook set up a
consistent state.
The trade-off is that three independent `add_menu_page` calls, three sets of
admin assets and three activation routines now share a single namespace. Every
one of those collided, and each collision is recorded in `KNOWN-ISSUES.md`.
## Layers
```
umbra-bookshelf.php (bootstrap)
├── constants one UMBRA_BOOKSHELF_* set
├── requires 21 class files, three modules
├── plugins_loaded construct modules
├── init text domain
└── admin_menu(9) single top-level menu
│
┌──────────────────────────┼──────────────────────────┐
│ │ │
includes/books includes/library includes/email
Umbra_Database Umbra_Bookshelf_Helper Umbra_Simple_Email
Umbra_Book_Manager …_Helper_Admin …_Admin
Umbra_Authors …_Helper_Install …_Install
Umbra_Series Umbra_Email_Sender
Umbra_Retail_Sites
Umbra_Rating_Sync
Umbra_Shortcodes
Umbra_SEO assets: admin/, public/, library/, email/
Umbra_GDPR assets/
Umbra_Goodreads_Scraper
Umbra_Amazon_Scraper
Umbra_Admin
Umbra_Migration
Umbra_Theme_Compatibility -> class_alias BD_BookShelfHelper
```
## Design rules
**One owner per name.** Every class, constant, asset handle, menu slug and AJAX
action is owned by exactly one module. The bootstrap owns the top-level menu and
the shared constants; modules own their submenus and assets.
**Persistence is not part of the identity.** Anything written to the database or
embedded in published content keeps its historical name. The rename covers
headers, text domains, classes, constants, handles, slugs and JS globals — never
table names, option keys, meta keys, cron hooks, nonces, shortcodes, post types,
taxonomies or cookies. See §2.2 of the specification for the full list.
**Registration order is explicit.** The top-level menu registers at
`admin_menu` priority 9 so the parent exists before modules append submenus at
the default 10. Modules construct on `plugins_loaded` so their own `init`
callbacks are registered before `init` fires.
**Compatibility shims are explicit.** `Umbra_Theme_Compatibility` exposes the
static API the unmodified theme calls, and re-exports the legacy
`BD_BookShelfHelper` name via `class_alias()`.
## Not resolved here
The catalogue and library modules keep two independent data models that happen
to share table names `bd_books` and `bd_series`. Consolidation made them
unconditionally conflict; it did not make them agree. Resolving this is the next
piece of work — see `KNOWN-ISSUES.md` §1 for the options.

View File

@@ -0,0 +1,132 @@
# Known issues
Carried into the Umbra Bookshelf consolidation. Items 1–4 are **pre-existing**
defects inherited from the three merged plugins; item 5 and the two repaired
below are noted for traceability.
---
## 1. `bd_books` and `bd_series` have two incompatible schemas (DEFERRED — unresolved)
The books and library modules both create tables with identical names and
different columns.
`Umbra_Database::table()` returns `$wpdb->prefix . 'bd_' . $name`, and the
catalogue issues `CREATE TABLE {prefix}bd_books` with **no** `IF NOT EXISTS`
guard:
```
catalogue wp_bd_books id, title, slug, description, isbn_*, amazon_asin,
goodreads_id, rating_*, created_at, ...
library wp_bd_books id, post_id, pen_name_id, series_id, volume_number,
asin, isbn, publish_date, sort_order, created_at
```
Same for `wp_bd_series` (catalogue: `name`, `slug`, `description` …;
library: `name`, `slug`, `cover_attachment_id`, `header_attachment_id`, `blurb`, …).
**Consequence.** Whichever module is activated first wins; the second either
no-ops (`IF NOT EXISTS`, library) or raises a database error (catalogue's
unguarded `CREATE TABLE`). Whichever schema exists first is what the other
module then reads and writes against, producing wrong results rather than an
obvious failure.
**Why deferred.** Resolving it means choosing a canonical schema and migrating
live data — a data-migration project, not a rename. Both datasets may hold rows
that matter.
**Options.**
1. Library schema becomes canonical; catalogue tables move to `bd_bookshelf_*`
with an upgrade routine copying existing rows. Requires renaming DB keys,
which this consolidation deliberately avoids.
2. Catalogue schema becomes canonical; library relies on the `book` post type and
postmeta instead of `bd_books`.
3. Delete the catalogue duplicates and keep only the library model.
---
## 2. Goodreads and Amazon scraping is implemented twice
* Catalogue: `Umbra_Goodreads_Scraper`, `Umbra_Amazon_Scraper`.
* Library: `Umbra_Bookshelf_Helper::refresh_goodreads_data()`,
`::scrape_amazon_ratings()`.
Both attach to the **same** cron hooks (`bd_bookshelf_goodreads_refresh`,
`bd_bookshelf_amazon_scrape`), so each scheduled run performs duplicate
requests to third-party APIs — wasted quota, faster rate limiting, and two
places to fix a parsing bug. Needs a decision on which implementation survives.
---
## 3. `assets/images/default-cover.png` was missing
`Umbra_Bookshelf_Helper` returns `assets/images/default-cover.png` as the
fallback cover, but the repository contained no images at all, so every
cover-less book rendered a broken image.
**Repaired during consolidation:** a neutral 600×900 placeholder PNG was
generated at that path. Replace it with real artwork before release.
---
## 4. `email/assets/js/admin.js` did not exist
`Umbra_Simple_Email_Admin::enqueue_assets()` enqueued it on every email admin
page, but no such file was ever committed — a guaranteed 404.
**Repaired during consolidation:** an empty placeholder was created with a
comment explaining the history. Add the intended behaviour, or drop the enqueue.
---
## 5. `Umbra_SEO` is never instantiated
`Umbra_SEO` registers its hooks from a constructor, but the bootstrap does not
construct it — carried over unchanged from the original BD Bookshelf bootstrap.
The class is loaded and its hooks never attach, so the module is inert.
Left as-is because enabling it changes behaviour. Either instantiate it in
`Umbra_Bookshelf::init_modules()` or delete it.
---
## 6. Licence metadata is inconsistent (NOT CHANGED)
Plugin headers declared `License: GPLv2 or later` while the repository's
`LICENSE`/`NOTICE` files (added in `85de97b`) and every SPDX header state
**Apache-2.0**. The consolidated header now says Apache-2.0 to match the SPDX
headers and `LICENSE`, but this needs a deliberate decision — WordPress
plugin-directory submissions require a GPL-compatible licence.
---
## Fixed during consolidation
* **`Cannot declare class` fatal.** `class-bdm-theme-compatibility.php` declared
`BD_BookShelfHelper`, the same class name as the library plugin's own helper.
Latent while the two were separate plugins (it only fired when both were
active), but an unconditional fatal once merged. The shim is now
`Umbra_Theme_Compatibility`, with a `class_alias()` preserving the legacy name
for the unmodified theme.
* **Duplicate `bd_bookshelf_import_csv` AJAX handler.** Both `Umbra_Admin` and
`Umbra_Migration` registered it; the second silently replaced the first. The
import page now lives solely in `Umbra_Admin` (it is the variant wired to
`admin/js/umbra-bookshelf-admin.js`); `Umbra_Migration` keeps only the
preview/run endpoints it uniquely provides.
* **Duplicate `bd-bookshelf-settings` submenu.** Catalogue and library both
claimed the slug, leaving one Settings screen unreachable. Now
`umbra-bookshelf-settings` and `umbra-bookshelf-library-settings`.
* **Duplicate admin menu.** All three modules called `add_menu_page`. Now one
top-level menu registered by the bootstrap at priority 9.
* **Colliding asset handles.** Catalogue and library both enqueued
`bd-bookshelf-admin`; the second enqueue was silently discarded. Handles are
now unique per module.
* **Modules could miss their own `init` callbacks.** Constructing modules from
inside an `init` callback means their `init` registrations race the hook
currently being dispatched. Modules are now constructed on `plugins_loaded`,
behind a re-entrancy guard.
* **Over-broad asset hook check.** The library module's
`strpos($hook, 'bd-bookshelf')` matched every catalogue and email screen once
all slugs shared the `umbra-bookshelf-` prefix. Now matched against its own
slug list.

View File

@@ -0,0 +1,11 @@
// Copyright 2026 Umbra Tools (umbratools.com)
// SPDX-License-Identifier: Apache-2.0
/**
* Umbra Bookshelf - Email module admin scripts.
*
* NOTE: Umbra_Simple_Email_Admin::enqueue_assets() has always enqueued this
* file, but the file was never present in the source repository, so every
* email admin page issued a 404 for it. It is created here as an empty
* placeholder so the enqueue resolves. Add email-module admin behaviour below.
*/

View File

@@ -5,14 +5,14 @@
/** /**
* Admin Interface class * Admin Interface class
* *
* @package BD_Bookshelf * @package Umbra_Bookshelf
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
class BDM_Admin { class Umbra_Admin {
/** /**
* Constructor * Constructor
@@ -44,44 +44,37 @@ class BDM_Admin {
* Add admin menu * Add admin menu
*/ */
public function add_admin_menu() { public function add_admin_menu() {
add_menu_page( // The top-level "Umbra Bookshelf" menu is registered once by the plugin
__('Books', 'bd-bookshelf'), // bootstrap (Umbra_Bookshelf::register_admin_menu); each module only
__('Books', 'bd-bookshelf'), // contributes submenus beneath it.
'manage_options', add_submenu_page('umbra-bookshelf', __('All Books', 'umbra-bookshelf'), __('All Books', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-all', [$this, 'render_books_page']);
'bd-bookshelf', add_submenu_page('umbra-bookshelf', __('Add New Book', 'umbra-bookshelf'), __('Add New Book', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-add', [$this, 'render_book_form']);
[$this, 'render_books_page'], add_submenu_page('umbra-bookshelf', __('Authors', 'umbra-bookshelf'), __('Authors', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-authors', [$this, 'render_authors_page']);
'dashicons-book', add_submenu_page('umbra-bookshelf', __('Series', 'umbra-bookshelf'), __('Series', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-series', [$this, 'render_series_page']);
30 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('bd-bookshelf', __('All Books', 'bd-bookshelf'), __('All Books', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf', [$this, 'render_books_page']); add_submenu_page('umbra-bookshelf', __('Import', 'umbra-bookshelf'), __('Import', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-import', [$this, 'render_import_page']);
add_submenu_page('bd-bookshelf', __('Add New Book', 'bd-bookshelf'), __('Add New Book', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-add', [$this, 'render_book_form']); add_submenu_page('umbra-bookshelf', __('Settings', 'umbra-bookshelf'), __('Settings', 'umbra-bookshelf'), 'manage_options', 'umbra-bookshelf-settings', [$this, 'render_settings_page']);
add_submenu_page('bd-bookshelf', __('Authors', 'bd-bookshelf'), __('Authors', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-authors', [$this, 'render_authors_page']);
add_submenu_page('bd-bookshelf', __('Series', 'bd-bookshelf'), __('Series', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-series', [$this, 'render_series_page']);
add_submenu_page('bd-bookshelf', __('Retail Sites', 'bd-bookshelf'), __('Retail Sites', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-sites', [$this, 'render_sites_page']);
add_submenu_page('bd-bookshelf', __('Shortcodes', 'bd-bookshelf'), __('Shortcodes', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-shortcodes', [$this, 'render_shortcodes_page']);
add_submenu_page('bd-bookshelf', __('Rating Sync', 'bd-bookshelf'), __('Rating Sync', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-sync', [$this, 'render_sync_page']);
add_submenu_page('bd-bookshelf', __('Settings', 'bd-bookshelf'), __('Settings', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-settings', [$this, 'render_settings_page']);
add_submenu_page('bd-bookshelf', __('Import', 'bd-bookshelf'), __('Import', 'bd-bookshelf'), 'manage_options', 'bd-bookshelf-import', [$this, 'render_import_page']);
} }
/** /**
* Enqueue admin scripts * Enqueue admin scripts
*/ */
public function enqueue_admin_scripts($hook) { public function enqueue_admin_scripts($hook) {
if (strpos($hook, 'bd-bookshelf') === false) { if (strpos($hook, 'umbra-bookshelf') === false) {
return; return;
} }
wp_enqueue_media(); wp_enqueue_media();
wp_enqueue_style('bd-bookshelf-admin', BD_BOOKSHELF_URL . 'admin/css/bd-bookshelf-admin.css', [], BD_BOOKSHELF_VERSION); wp_enqueue_style('umbra-bookshelf-admin', UMBRA_BOOKSHELF_URL . 'admin/css/umbra-bookshelf-admin.css', [], UMBRA_BOOKSHELF_VERSION);
wp_enqueue_script('bd-bookshelf-admin', BD_BOOKSHELF_URL . 'admin/js/bd-bookshelf-admin.js', ['jquery'], BD_BOOKSHELF_VERSION, true); wp_enqueue_script('umbra-bookshelf-admin', UMBRA_BOOKSHELF_URL . 'admin/js/umbra-bookshelf-admin.js', ['jquery'], UMBRA_BOOKSHELF_VERSION, true);
wp_localize_script('bd-bookshelf-admin', 'bdBookshelfAdmin', [ wp_localize_script('umbra-bookshelf-admin', 'umbraBookshelfAdmin', [
'ajaxurl' => admin_url('admin-ajax.php'), 'ajaxurl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('bd_bookshelf_admin_nonce'), 'nonce' => wp_create_nonce('bd_bookshelf_admin_nonce'),
'mediaTitle' => __('Select Cover Image', 'bd-bookshelf'), 'mediaTitle' => __('Select Cover Image', 'umbra-bookshelf'),
'mediaButton' => __('Use This Image', 'bd-bookshelf'), 'mediaButton' => __('Use This Image', 'umbra-bookshelf'),
]); ]);
} }
@@ -90,29 +83,29 @@ class BDM_Admin {
*/ */
public function render_books_page() { public function render_books_page() {
if (!current_user_can('manage_options')) { if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf')); wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
} }
$books = BDM_Book_Manager::get_books(['number' => 50]); $books = Umbra_Book_Manager::get_books(['number' => 50]);
?> ?>
<div class="wrap"> <div class="wrap">
<h1 class="wp-heading-inline"><?php esc_html_e('Books', 'bd-bookshelf'); ?></h1> <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', 'bd-bookshelf'); ?></a> <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"> <hr class="wp-header-end">
<table class="wp-list-table widefat fixed striped"> <table class="wp-list-table widefat fixed striped">
<thead> <thead>
<tr> <tr>
<th style="width:30%"><?php esc_html_e('Title', 'bd-bookshelf'); ?></th> <th style="width:30%"><?php esc_html_e('Title', 'umbra-bookshelf'); ?></th>
<th style="width:15%"><?php esc_html_e('ISBNs', 'bd-bookshelf'); ?></th> <th style="width:15%"><?php esc_html_e('ISBNs', 'umbra-bookshelf'); ?></th>
<th style="width:15%"><?php esc_html_e('Ratings', 'bd-bookshelf'); ?></th> <th style="width:15%"><?php esc_html_e('Ratings', 'umbra-bookshelf'); ?></th>
<th style="width:15%"><?php esc_html_e('Last Sync', 'bd-bookshelf'); ?></th> <th style="width:15%"><?php esc_html_e('Last Sync', 'umbra-bookshelf'); ?></th>
<th style="width:15%"><?php esc_html_e('Actions', 'bd-bookshelf'); ?></th> <th style="width:15%"><?php esc_html_e('Actions', 'umbra-bookshelf'); ?></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php if (empty($books)) : ?> <?php if (empty($books)) : ?>
<tr><td colspan="5"><?php esc_html_e('No books found. Click "Add New Book" to get started.', 'bd-bookshelf'); ?></td></tr> <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 else : ?>
<?php foreach ($books as $book) : ?> <?php foreach ($books as $book) : ?>
<tr> <tr>
@@ -137,8 +130,8 @@ class BDM_Admin {
</td> </td>
<td><?php echo $book->last_sync ? date('M j, Y g:i a', strtotime($book->last_sync)) : '-'; ?></td> <td><?php echo $book->last_sync ? date('M j, Y g:i a', strtotime($book->last_sync)) : '-'; ?></td>
<td> <td>
<a href="<?php echo admin_url('admin.php?page=bd-bookshelf-add&book_id=' . $book->id); ?>"><?php esc_html_e('Edit', 'bd-bookshelf'); ?></a> | <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', 'bd-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> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
@@ -154,45 +147,45 @@ class BDM_Admin {
*/ */
public function render_book_form() { public function render_book_form() {
if (!current_user_can('manage_options')) { if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf')); 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_id = isset($_GET['book_id']) ? absint($_GET['book_id']) : 0;
$book = $book_id ? BDM_Book_Manager::get_book($book_id) : null; $book = $book_id ? Umbra_Book_Manager::get_book($book_id) : null;
$series = BDM_Series::get_series(['number' => 100]); $series = Umbra_Series::get_series(['number' => 100]);
$authors = BDM_Authors::get_authors(['number' => 100]); $authors = Umbra_Authors::get_authors(['number' => 100]);
// Get existing retail links grouped by format and site // Get existing retail links grouped by format and site
$existing_links = []; $existing_links = [];
if ($book) { if ($book) {
$raw_links = BDM_Book_Manager::get_book_retail_links($book_id); $raw_links = Umbra_Book_Manager::get_book_retail_links($book_id);
foreach ($raw_links as $link) { foreach ($raw_links as $link) {
$existing_links[$link->format][$link->site_name] = $link->url; $existing_links[$link->format][$link->site_name] = $link->url;
} }
} }
// Retail sites for URL inputs // Retail sites for URL inputs
$retail_sites = BDM_Retail_Sites::get_sites(true); $retail_sites = Umbra_Retail_Sites::get_sites(true);
?> ?>
<div class="wrap"> <div class="wrap">
<h1><?php echo $book ? esc_html__('Edit Book', 'bd-bookshelf') : esc_html__('Add New Book', 'bd-bookshelf'); ?></h1> <h1><?php echo $book ? esc_html__('Edit Book', 'umbra-bookshelf') : esc_html__('Add New Book', 'umbra-bookshelf'); ?></h1>
<form id="bd-bookshelf-book-form"> <form id="bd-bookshelf-book-form">
<?php wp_nonce_field('bd_bookshelf_save_book', 'nonce'); ?> <?php wp_nonce_field('bd_bookshelf_save_book', 'nonce'); ?>
<input type="hidden" name="book_id" value="<?php echo $book_id; ?>"> <input type="hidden" name="book_id" value="<?php echo $book_id; ?>">
<h2><?php esc_html_e('Book Details', 'bd-bookshelf'); ?></h2> <h2><?php esc_html_e('Book Details', 'umbra-bookshelf'); ?></h2>
<table class="form-table"> <table class="form-table">
<tr> <tr>
<th><label for="title"><?php esc_html_e('Title', 'bd-bookshelf'); ?> <span class="required">*</span></label></th> <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> <td><input type="text" name="title" id="title" value="<?php echo $book ? esc_attr($book->title) : ''; ?>" class="regular-text" required></td>
</tr> </tr>
<tr> <tr>
<th><label for="description"><?php esc_html_e('Description', 'bd-bookshelf'); ?></label></th> <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> <td><textarea name="description" id="description" rows="5" class="large-text"><?php echo $book ? esc_textarea($book->description) : ''; ?></textarea></td>
</tr> </tr>
<tr> <tr>
<th><label><?php esc_html_e('Cover Image', 'bd-bookshelf'); ?></label></th> <th><label><?php esc_html_e('Cover Image', 'umbra-bookshelf'); ?></label></th>
<td> <td>
<div class="bd-bookshelf-cover-upload"> <div class="bd-bookshelf-cover-upload">
<input type="hidden" name="cover_image" id="cover_image" value="<?php echo $book ? esc_url($book->cover_image) : ''; ?>"> <input type="hidden" name="cover_image" id="cover_image" value="<?php echo $book ? esc_url($book->cover_image) : ''; ?>">
@@ -201,27 +194,27 @@ class BDM_Admin {
<img src="<?php echo esc_url($book->cover_image); ?>" style="max-width:200px;height:auto"> <img src="<?php echo esc_url($book->cover_image); ?>" style="max-width:200px;height:auto">
<?php endif; ?> <?php endif; ?>
</div> </div>
<button type="button" id="cover-upload-btn" class="button"><?php esc_html_e('Select or Upload Cover', 'bd-bookshelf'); ?></button> <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', 'bd-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> </div>
</td> </td>
</tr> </tr>
<tr> <tr>
<th><label for="series_id"><?php esc_html_e('Series', 'bd-bookshelf'); ?></label></th> <th><label for="series_id"><?php esc_html_e('Series', 'umbra-bookshelf'); ?></label></th>
<td> <td>
<select name="series_id" id="series_id"> <select name="series_id" id="series_id">
<option value="0"><?php esc_html_e('None', 'bd-bookshelf'); ?></option> <option value="0"><?php esc_html_e('None', 'umbra-bookshelf'); ?></option>
<?php foreach ($series as $s) : ?> <?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> <option value="<?php echo $s->id; ?>" <?php selected($book->series_id ?? 0, $s->id); ?>><?php echo esc_html($s->name); ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </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', 'bd-bookshelf'); ?></a> <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> </td>
</tr> </tr>
</table> </table>
<h2><?php esc_html_e('Authors', 'bd-bookshelf'); ?></h2> <h2><?php esc_html_e('Authors', 'umbra-bookshelf'); ?></h2>
<p class="description"><?php esc_html_e('Select up to 3 authors for this book.', 'bd-bookshelf'); ?></p> <p class="description"><?php esc_html_e('Select up to 3 authors for this book.', 'umbra-bookshelf'); ?></p>
<div id="bd-bookshelf-authors-container"> <div id="bd-bookshelf-authors-container">
<?php <?php
$book_authors = []; $book_authors = [];
@@ -235,7 +228,7 @@ class BDM_Admin {
?> ?>
<div class="bd-bookshelf-author-row"> <div class="bd-bookshelf-author-row">
<select name="authors[]" class="bd-bookshelf-author-select"> <select name="authors[]" class="bd-bookshelf-author-select">
<option value="0"><?php esc_html_e('-- Select Author --', 'bd-bookshelf'); ?></option> <option value="0"><?php esc_html_e('-- Select Author --', 'umbra-bookshelf'); ?></option>
<?php foreach ($authors as $a) : ?> <?php foreach ($authors as $a) : ?>
<option value="<?php echo $a->id; ?>" <?php selected($selected, $a->id); ?>><?php echo esc_html($a->display_name); ?></option> <option value="<?php echo $a->id; ?>" <?php selected($selected, $a->id); ?>><?php echo esc_html($a->display_name); ?></option>
<?php endforeach; ?> <?php endforeach; ?>
@@ -243,35 +236,35 @@ class BDM_Admin {
</div> </div>
<?php endfor; ?> <?php endfor; ?>
</div> </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', 'bd-bookshelf'); ?></a> <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', 'bd-bookshelf'); ?></h2> <h2><?php esc_html_e('ISBNs & ASINs', 'umbra-bookshelf'); ?></h2>
<table class="wp-list-table widefat fixed striped bd-bookshelf-isbn-grid"> <table class="wp-list-table widefat fixed striped bd-bookshelf-isbn-grid">
<thead> <thead>
<tr> <tr>
<th style="width:120px"><?php esc_html_e('Format', 'bd-bookshelf'); ?></th> <th style="width:120px"><?php esc_html_e('Format', 'umbra-bookshelf'); ?></th>
<th><?php esc_html_e('ISBN', 'bd-bookshelf'); ?></th> <th><?php esc_html_e('ISBN', 'umbra-bookshelf'); ?></th>
<th><?php esc_html_e('Amazon ASIN', 'bd-bookshelf'); ?></th> <th><?php esc_html_e('Amazon ASIN', 'umbra-bookshelf'); ?></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td><strong><?php esc_html_e('eBook', 'bd-bookshelf'); ?></strong></td> <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="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> <td><input type="text" name="asin_ebook" value="<?php echo $book ? esc_attr($book->asin_ebook ?? '') : ''; ?>" class="regular-text"></td>
</tr> </tr>
<tr> <tr>
<td><strong><?php esc_html_e('Paperback', 'bd-bookshelf'); ?></strong></td> <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="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> <td><input type="text" name="asin_paperback" value="<?php echo $book ? esc_attr($book->asin_paperback ?? '') : ''; ?>" class="regular-text"></td>
</tr> </tr>
<tr> <tr>
<td><strong><?php esc_html_e('Hardcover', 'bd-bookshelf'); ?></strong></td> <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="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> <td><input type="text" name="asin_hardcover" value="<?php echo $book ? esc_attr($book->asin_hardcover ?? '') : ''; ?>" class="regular-text"></td>
</tr> </tr>
<tr> <tr>
<td><strong><?php esc_html_e('Audio', 'bd-bookshelf'); ?></strong></td> <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="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> <td><input type="text" name="asin_audio" value="<?php echo $book ? esc_attr($book->asin_audio ?? '') : ''; ?>" class="regular-text"></td>
</tr> </tr>
@@ -279,8 +272,8 @@ class BDM_Admin {
</table> </table>
<input type="hidden" name="goodreads_id" value="<?php echo $book ? esc_attr($book->goodreads_id) : ''; ?>"> <input type="hidden" name="goodreads_id" value="<?php echo $book ? esc_attr($book->goodreads_id) : ''; ?>">
<h2><?php esc_html_e('Retail Links', 'bd-bookshelf'); ?></h2> <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.', 'bd-bookshelf'); ?></p> <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 <?php
$formats_config = [ $formats_config = [
@@ -322,7 +315,7 @@ class BDM_Admin {
<?php endforeach; ?> <?php endforeach; ?>
<p class="submit"> <p class="submit">
<button type="submit" class="button button-primary button-hero"><?php esc_html_e('Save Book', 'bd-bookshelf'); ?></button> <button type="submit" class="button button-primary button-hero"><?php esc_html_e('Save Book', 'umbra-bookshelf'); ?></button>
</p> </p>
</form> </form>
</div> </div>
@@ -334,29 +327,29 @@ class BDM_Admin {
*/ */
public function render_authors_page() { public function render_authors_page() {
if (!current_user_can('manage_options')) { if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf')); wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
} }
$authors = BDM_Authors::get_authors(['number' => 100]); $authors = Umbra_Authors::get_authors(['number' => 100]);
?> ?>
<div class="wrap"> <div class="wrap">
<h1 class="wp-heading-inline"><?php esc_html_e('Authors', 'bd-bookshelf'); ?></h1> <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', 'bd-bookshelf'); ?></button> <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"> <hr class="wp-header-end">
<table class="wp-list-table widefat fixed striped"> <table class="wp-list-table widefat fixed striped">
<thead> <thead>
<tr> <tr>
<th style="width:20%"><?php esc_html_e('Name', 'bd-bookshelf'); ?></th> <th style="width:20%"><?php esc_html_e('Name', 'umbra-bookshelf'); ?></th>
<th style="width:20%"><?php esc_html_e('Email', 'bd-bookshelf'); ?></th> <th style="width:20%"><?php esc_html_e('Email', 'umbra-bookshelf'); ?></th>
<th style="width:15%"><?php esc_html_e('Website', 'bd-bookshelf'); ?></th> <th style="width:15%"><?php esc_html_e('Website', 'umbra-bookshelf'); ?></th>
<th style="width:15%"><?php esc_html_e('Goodreads ID', 'bd-bookshelf'); ?></th> <th style="width:15%"><?php esc_html_e('Goodreads ID', 'umbra-bookshelf'); ?></th>
<th style="width:15%"><?php esc_html_e('Actions', 'bd-bookshelf'); ?></th> <th style="width:15%"><?php esc_html_e('Actions', 'umbra-bookshelf'); ?></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php if (empty($authors)) : ?> <?php if (empty($authors)) : ?>
<tr><td colspan="5"><?php esc_html_e('No authors found.', 'bd-bookshelf'); ?></td></tr> <tr><td colspan="5"><?php esc_html_e('No authors found.', 'umbra-bookshelf'); ?></td></tr>
<?php else : ?> <?php else : ?>
<?php foreach ($authors as $author) : ?> <?php foreach ($authors as $author) : ?>
<tr> <tr>
@@ -365,8 +358,8 @@ class BDM_Admin {
<td><?php echo $author->website ? '<a href="' . esc_url($author->website) . '" target="_blank">' . esc_html($author->website) . '</a>' : '-'; ?></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><?php echo esc_html($author->goodreads_author_id); ?></td>
<td> <td>
<a href="#" class="bd-bookshelf-edit-author" data-id="<?php echo $author->id; ?>"><?php esc_html_e('Edit', 'bd-bookshelf'); ?></a> | <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', 'bd-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> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
@@ -379,7 +372,7 @@ class BDM_Admin {
<div id="bd-bookshelf-author-modal" class="bd-bookshelf-modal" style="display:none"> <div id="bd-bookshelf-author-modal" class="bd-bookshelf-modal" style="display:none">
<div class="bd-bookshelf-modal-content"> <div class="bd-bookshelf-modal-content">
<div class="bd-bookshelf-modal-header"> <div class="bd-bookshelf-modal-header">
<h2 id="bd-bookshelf-author-modal-title"><?php esc_html_e('Add New Author', 'bd-bookshelf'); ?></h2> <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">&times;</button> <button type="button" class="bd-bookshelf-modal-close">&times;</button>
</div> </div>
<form id="bd-bookshelf-author-form"> <form id="bd-bookshelf-author-form">
@@ -387,37 +380,37 @@ class BDM_Admin {
<input type="hidden" name="author_id" id="author_id" value="0"> <input type="hidden" name="author_id" id="author_id" value="0">
<table class="form-table"> <table class="form-table">
<tr> <tr>
<th><label for="author_display_name"><?php esc_html_e('Display Name', 'bd-bookshelf'); ?> <span class="required">*</span></label></th> <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> <td><input type="text" name="display_name" id="author_display_name" class="regular-text" required></td>
</tr> </tr>
<tr> <tr>
<th><label for="author_email"><?php esc_html_e('Email', 'bd-bookshelf'); ?></label></th> <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> <td><input type="email" name="email" id="author_email" class="regular-text"></td>
</tr> </tr>
<tr> <tr>
<th><label for="author_website"><?php esc_html_e('Website', 'bd-bookshelf'); ?></label></th> <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> <td><input type="url" name="website" id="author_website" class="regular-text"></td>
</tr> </tr>
<tr> <tr>
<th><label for="author_bio"><?php esc_html_e('Bio', 'bd-bookshelf'); ?></label></th> <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> <td><textarea name="bio" id="author_bio" rows="4" class="large-text"></textarea></td>
</tr> </tr>
<tr> <tr>
<th><label for="author_goodreads_id"><?php esc_html_e('Goodreads Author ID', 'bd-bookshelf'); ?></label></th> <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> <td><input type="text" name="goodreads_author_id" id="author_goodreads_id" class="regular-text"></td>
</tr> </tr>
<tr> <tr>
<th><label><?php esc_html_e('Photo', 'bd-bookshelf'); ?></label></th> <th><label><?php esc_html_e('Photo', 'umbra-bookshelf'); ?></label></th>
<td> <td>
<input type="hidden" name="photo_url" id="author_photo_url" value=""> <input type="hidden" name="photo_url" id="author_photo_url" value="">
<div id="author-photo-preview" style="margin-bottom:10px"></div> <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', 'bd-bookshelf'); ?></button> <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', 'bd-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> </td>
</tr> </tr>
</table> </table>
<p class="submit"> <p class="submit">
<button type="submit" class="button button-primary"><?php esc_html_e('Save Author', 'bd-bookshelf'); ?></button> <button type="submit" class="button button-primary"><?php esc_html_e('Save Author', 'umbra-bookshelf'); ?></button>
</p> </p>
</form> </form>
</div> </div>
@@ -430,35 +423,35 @@ class BDM_Admin {
*/ */
public function render_series_page() { public function render_series_page() {
if (!current_user_can('manage_options')) { if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf')); wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
} }
$series = BDM_Series::get_series(['number' => 100]); $series = Umbra_Series::get_series(['number' => 100]);
?> ?>
<div class="wrap"> <div class="wrap">
<h1 class="wp-heading-inline"><?php esc_html_e('Series', 'bd-bookshelf'); ?></h1> <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', 'bd-bookshelf'); ?></button> <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"> <hr class="wp-header-end">
<table class="wp-list-table widefat fixed striped"> <table class="wp-list-table widefat fixed striped">
<thead> <thead>
<tr> <tr>
<th style="width:30%"><?php esc_html_e('Name', 'bd-bookshelf'); ?></th> <th style="width:30%"><?php esc_html_e('Name', 'umbra-bookshelf'); ?></th>
<th style="width:50%"><?php esc_html_e('Description', 'bd-bookshelf'); ?></th> <th style="width:50%"><?php esc_html_e('Description', 'umbra-bookshelf'); ?></th>
<th style="width:20%"><?php esc_html_e('Actions', 'bd-bookshelf'); ?></th> <th style="width:20%"><?php esc_html_e('Actions', 'umbra-bookshelf'); ?></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php if (empty($series)) : ?> <?php if (empty($series)) : ?>
<tr><td colspan="3"><?php esc_html_e('No series found.', 'bd-bookshelf'); ?></td></tr> <tr><td colspan="3"><?php esc_html_e('No series found.', 'umbra-bookshelf'); ?></td></tr>
<?php else : ?> <?php else : ?>
<?php foreach ($series as $s) : ?> <?php foreach ($series as $s) : ?>
<tr> <tr>
<td><strong><?php echo esc_html($s->name); ?></strong></td> <td><strong><?php echo esc_html($s->name); ?></strong></td>
<td><?php echo esc_html(wp_trim_words($s->description, 15)); ?></td> <td><?php echo esc_html(wp_trim_words($s->description, 15)); ?></td>
<td> <td>
<a href="#" class="bd-bookshelf-edit-series" data-id="<?php echo $s->id; ?>"><?php esc_html_e('Edit', 'bd-bookshelf'); ?></a> | <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', 'bd-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> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
@@ -471,7 +464,7 @@ class BDM_Admin {
<div id="bd-bookshelf-series-modal" class="bd-bookshelf-modal" style="display:none"> <div id="bd-bookshelf-series-modal" class="bd-bookshelf-modal" style="display:none">
<div class="bd-bookshelf-modal-content"> <div class="bd-bookshelf-modal-content">
<div class="bd-bookshelf-modal-header"> <div class="bd-bookshelf-modal-header">
<h2 id="bd-bookshelf-series-modal-title"><?php esc_html_e('Add New Series', 'bd-bookshelf'); ?></h2> <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">&times;</button> <button type="button" class="bd-bookshelf-modal-close">&times;</button>
</div> </div>
<form id="bd-bookshelf-series-form"> <form id="bd-bookshelf-series-form">
@@ -479,16 +472,16 @@ class BDM_Admin {
<input type="hidden" name="series_id" id="series_id_input" value="0"> <input type="hidden" name="series_id" id="series_id_input" value="0">
<table class="form-table"> <table class="form-table">
<tr> <tr>
<th><label for="series_name"><?php esc_html_e('Series Name', 'bd-bookshelf'); ?> <span class="required">*</span></label></th> <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> <td><input type="text" name="name" id="series_name" class="regular-text" required></td>
</tr> </tr>
<tr> <tr>
<th><label for="series_description"><?php esc_html_e('Description', 'bd-bookshelf'); ?></label></th> <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> <td><textarea name="description" id="series_description" rows="4" class="large-text"></textarea></td>
</tr> </tr>
</table> </table>
<p class="submit"> <p class="submit">
<button type="submit" class="button button-primary"><?php esc_html_e('Save Series', 'bd-bookshelf'); ?></button> <button type="submit" class="button button-primary"><?php esc_html_e('Save Series', 'umbra-bookshelf'); ?></button>
</p> </p>
</form> </form>
</div> </div>
@@ -501,14 +494,14 @@ class BDM_Admin {
*/ */
public function render_sites_page() { public function render_sites_page() {
if (!current_user_can('manage_options')) { if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf')); wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
} }
$sites = BDM_Retail_Sites::get_sites(false); $sites = Umbra_Retail_Sites::get_sites(false);
?> ?>
<div class="wrap"> <div class="wrap">
<h1><?php esc_html_e('Retail Sites & Affiliate Codes', 'bd-bookshelf'); ?></h1> <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.', 'bd-bookshelf'); ?></p> <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"> <form id="bd-bookshelf-sites-form">
<?php wp_nonce_field('bd_bookshelf_admin_nonce', 'sites_nonce'); ?> <?php wp_nonce_field('bd_bookshelf_admin_nonce', 'sites_nonce'); ?>
@@ -516,10 +509,10 @@ class BDM_Admin {
<table class="wp-list-table widefat fixed striped"> <table class="wp-list-table widefat fixed striped">
<thead> <thead>
<tr> <tr>
<th style="width:25%"><?php esc_html_e('Site', 'bd-bookshelf'); ?></th> <th style="width:25%"><?php esc_html_e('Site', 'umbra-bookshelf'); ?></th>
<th style="width:35%"><?php esc_html_e('URL Pattern', 'bd-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', 'bd-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', 'bd-bookshelf'); ?></th> <th style="width:25%"><?php esc_html_e('Your Affiliate Code', 'umbra-bookshelf'); ?></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -529,7 +522,7 @@ class BDM_Admin {
<td><code style="font-size:11px"><?php echo esc_html($site->url_pattern); ?></code></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><?php echo esc_html($site->affiliate_param); ?></td>
<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', 'bd-bookshelf'); ?>"> <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> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
@@ -537,7 +530,7 @@ class BDM_Admin {
</table> </table>
<p class="submit"> <p class="submit">
<button type="submit" class="button button-primary button-hero"><?php esc_html_e('Save Affiliate Codes', 'bd-bookshelf'); ?></button> <button type="submit" class="button button-primary button-hero"><?php esc_html_e('Save Affiliate Codes', 'umbra-bookshelf'); ?></button>
</p> </p>
</form> </form>
</div> </div>
@@ -549,33 +542,33 @@ class BDM_Admin {
*/ */
public function render_shortcodes_page() { public function render_shortcodes_page() {
if (!current_user_can('manage_options')) { if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf')); wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
} }
global $wpdb; global $wpdb;
$table = BDM_Database::table('shortcodes'); $table = Umbra_Database::table('shortcodes');
$shortcodes = $wpdb->get_results("SELECT * FROM {$table} ORDER BY created_at DESC"); $shortcodes = $wpdb->get_results("SELECT * FROM {$table} ORDER BY created_at DESC");
?> ?>
<div class="wrap"> <div class="wrap">
<h1 class="wp-heading-inline"><?php esc_html_e('Custom Shortcodes', 'bd-bookshelf'); ?></h1> <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', 'bd-bookshelf'); ?></button> <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"> <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.', 'bd-bookshelf'); ?></p> <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"> <table class="wp-list-table widefat fixed striped">
<thead> <thead>
<tr> <tr>
<th style="width:20%"><?php esc_html_e('Shortcode', 'bd-bookshelf'); ?></th> <th style="width:20%"><?php esc_html_e('Shortcode', 'umbra-bookshelf'); ?></th>
<th style="width:30%"><?php esc_html_e('Target URL', 'bd-bookshelf'); ?></th> <th style="width:30%"><?php esc_html_e('Target URL', 'umbra-bookshelf'); ?></th>
<th style="width:15%"><?php esc_html_e('Redirect', 'bd-bookshelf'); ?></th> <th style="width:15%"><?php esc_html_e('Redirect', 'umbra-bookshelf'); ?></th>
<th style="width:15%"><?php esc_html_e('Clicks', 'bd-bookshelf'); ?></th> <th style="width:15%"><?php esc_html_e('Clicks', 'umbra-bookshelf'); ?></th>
<th style="width:20%"><?php esc_html_e('Actions', 'bd-bookshelf'); ?></th> <th style="width:20%"><?php esc_html_e('Actions', 'umbra-bookshelf'); ?></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php if (empty($shortcodes)) : ?> <?php if (empty($shortcodes)) : ?>
<tr><td colspan="5"><?php esc_html_e('No shortcodes found.', 'bd-bookshelf'); ?></td></tr> <tr><td colspan="5"><?php esc_html_e('No shortcodes found.', 'umbra-bookshelf'); ?></td></tr>
<?php else : ?> <?php else : ?>
<?php foreach ($shortcodes as $sc) : ?> <?php foreach ($shortcodes as $sc) : ?>
<tr> <tr>
@@ -584,8 +577,8 @@ class BDM_Admin {
<td><?php echo esc_html($sc->redirect_type); ?></td> <td><?php echo esc_html($sc->redirect_type); ?></td>
<td><?php echo number_format($sc->click_count); ?></td> <td><?php echo number_format($sc->click_count); ?></td>
<td> <td>
<a href="#" class="bd-bookshelf-edit-shortcode" data-id="<?php echo $sc->id; ?>"><?php esc_html_e('Edit', 'bd-bookshelf'); ?></a> | <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', 'bd-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> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
@@ -598,7 +591,7 @@ class BDM_Admin {
<div id="bd-bookshelf-shortcode-modal" class="bd-bookshelf-modal" style="display:none"> <div id="bd-bookshelf-shortcode-modal" class="bd-bookshelf-modal" style="display:none">
<div class="bd-bookshelf-modal-content"> <div class="bd-bookshelf-modal-content">
<div class="bd-bookshelf-modal-header"> <div class="bd-bookshelf-modal-header">
<h2 id="bd-bookshelf-shortcode-modal-title"><?php esc_html_e('Add New Shortcode', 'bd-bookshelf'); ?></h2> <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">&times;</button> <button type="button" class="bd-bookshelf-modal-close">&times;</button>
</div> </div>
<form id="bd-bookshelf-shortcode-form"> <form id="bd-bookshelf-shortcode-form">
@@ -606,18 +599,18 @@ class BDM_Admin {
<input type="hidden" name="shortcode_id" id="shortcode_id" value="0"> <input type="hidden" name="shortcode_id" id="shortcode_id" value="0">
<table class="form-table"> <table class="form-table">
<tr> <tr>
<th><label for="shortcode_slug"><?php esc_html_e('Shortcode Slug', 'bd-bookshelf'); ?> <span class="required">*</span></label></th> <th><label for="shortcode_slug"><?php esc_html_e('Shortcode Slug', 'umbra-bookshelf'); ?> <span class="required">*</span></label></th>
<td> <td>
<input type="text" name="slug" id="shortcode_slug" class="regular-text" required pattern="[a-z0-9\-]+"> <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]', 'bd-bookshelf'); ?></p> <p class="description"><?php esc_html_e('Only lowercase letters, numbers, and hyphens. Use as: [your-slug]', 'umbra-bookshelf'); ?></p>
</td> </td>
</tr> </tr>
<tr> <tr>
<th><label for="shortcode_target"><?php esc_html_e('Target URL', 'bd-bookshelf'); ?> <span class="required">*</span></label></th> <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> <td><input type="url" name="target_url" id="shortcode_target" class="regular-text" required></td>
</tr> </tr>
<tr> <tr>
<th><label for="shortcode_redirect"><?php esc_html_e('Redirect Type', 'bd-bookshelf'); ?></label></th> <th><label for="shortcode_redirect"><?php esc_html_e('Redirect Type', 'umbra-bookshelf'); ?></label></th>
<td> <td>
<select name="redirect_type" id="shortcode_redirect"> <select name="redirect_type" id="shortcode_redirect">
<option value="301">301 - Permanent</option> <option value="301">301 - Permanent</option>
@@ -627,12 +620,12 @@ class BDM_Admin {
</td> </td>
</tr> </tr>
<tr> <tr>
<th><label for="shortcode_description"><?php esc_html_e('Description', 'bd-bookshelf'); ?></label></th> <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> <td><input type="text" name="description" id="shortcode_description" class="regular-text"></td>
</tr> </tr>
</table> </table>
<p class="submit"> <p class="submit">
<button type="submit" class="button button-primary"><?php esc_html_e('Save Shortcode', 'bd-bookshelf'); ?></button> <button type="submit" class="button button-primary"><?php esc_html_e('Save Shortcode', 'umbra-bookshelf'); ?></button>
</p> </p>
</form> </form>
</div> </div>
@@ -645,42 +638,42 @@ class BDM_Admin {
*/ */
public function render_sync_page() { public function render_sync_page() {
if (!current_user_can('manage_options')) { if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf')); wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
} }
$last_sync = BDM_Rating_Sync::get_last_sync_info(); $last_sync = Umbra_Rating_Sync::get_last_sync_info();
?> ?>
<div class="wrap"> <div class="wrap">
<h1><?php esc_html_e('Rating Sync', 'bd-bookshelf'); ?></h1> <h1><?php esc_html_e('Rating Sync', 'umbra-bookshelf'); ?></h1>
<div class="bd-bookshelf-import-section"> <div class="bd-bookshelf-import-section">
<h2><?php esc_html_e('Sync Status', 'bd-bookshelf'); ?></h2> <h2><?php esc_html_e('Sync Status', 'umbra-bookshelf'); ?></h2>
<?php if (!empty($last_sync)) : ?> <?php if (!empty($last_sync)) : ?>
<table class="form-table"> <table class="form-table">
<tr> <tr>
<th><?php esc_html_e('Last Sync Time', 'bd-bookshelf'); ?></th> <th><?php esc_html_e('Last Sync Time', 'umbra-bookshelf'); ?></th>
<td><?php echo esc_html($last_sync['time']); ?></td> <td><?php echo esc_html($last_sync['time']); ?></td>
</tr> </tr>
<tr> <tr>
<th><?php esc_html_e('Books Synced', 'bd-bookshelf'); ?></th> <th><?php esc_html_e('Books Synced', 'umbra-bookshelf'); ?></th>
<td><?php echo esc_html($last_sync['synced']); ?></td> <td><?php echo esc_html($last_sync['synced']); ?></td>
</tr> </tr>
<tr> <tr>
<th><?php esc_html_e('Books Failed', 'bd-bookshelf'); ?></th> <th><?php esc_html_e('Books Failed', 'umbra-bookshelf'); ?></th>
<td><?php echo esc_html($last_sync['failed']); ?></td> <td><?php echo esc_html($last_sync['failed']); ?></td>
</tr> </tr>
<tr> <tr>
<th><?php esc_html_e('Total Books', 'bd-bookshelf'); ?></th> <th><?php esc_html_e('Total Books', 'umbra-bookshelf'); ?></th>
<td><?php echo esc_html($last_sync['total']); ?></td> <td><?php echo esc_html($last_sync['total']); ?></td>
</tr> </tr>
</table> </table>
<?php else : ?> <?php else : ?>
<p><?php esc_html_e('No sync has been performed yet.', 'bd-bookshelf'); ?></p> <p><?php esc_html_e('No sync has been performed yet.', 'umbra-bookshelf'); ?></p>
<?php endif; ?> <?php endif; ?>
<p class="submit"> <p class="submit">
<button type="button" id="bd-bookshelf-manual-sync" class="button button-primary"><?php esc_html_e('Sync Now', 'bd-bookshelf'); ?></button> <button type="button" id="bd-bookshelf-manual-sync" class="button button-primary"><?php esc_html_e('Sync Now', 'umbra-bookshelf'); ?></button>
</p> </p>
</div> </div>
</div> </div>
@@ -692,28 +685,28 @@ class BDM_Admin {
*/ */
public function render_settings_page() { public function render_settings_page() {
if (!current_user_can('manage_options')) { if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf')); wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
} }
?> ?>
<div class="wrap"> <div class="wrap">
<h1><?php esc_html_e('Settings', 'bd-bookshelf'); ?></h1> <h1><?php esc_html_e('Settings', 'umbra-bookshelf'); ?></h1>
<form id="bd-bookshelf-settings-form"> <form id="bd-bookshelf-settings-form">
<?php wp_nonce_field('bd_bookshelf_save_settings', 'nonce'); ?> <?php wp_nonce_field('bd_bookshelf_save_settings', 'nonce'); ?>
<h2><?php esc_html_e('GDPR Settings', 'bd-bookshelf'); ?></h2> <h2><?php esc_html_e('GDPR Settings', 'umbra-bookshelf'); ?></h2>
<table class="form-table"> <table class="form-table">
<tr> <tr>
<th><label for="gdpr_text"><?php esc_html_e('Consent Banner Text', 'bd-bookshelf'); ?></label></th> <th><label for="gdpr_text"><?php esc_html_e('Consent Banner Text', 'umbra-bookshelf'); ?></label></th>
<td> <td>
<textarea name="gdpr_text" id="gdpr_text" rows="3" class="large-text"><?php echo esc_textarea(BDM_GDPR::get_consent_text()); ?></textarea> <textarea name="gdpr_text" id="gdpr_text" rows="3" class="large-text"><?php echo esc_textarea(Umbra_GDPR::get_consent_text()); ?></textarea>
</td> </td>
</tr> </tr>
</table> </table>
<p class="submit"> <p class="submit">
<button type="submit" class="button button-primary"><?php esc_html_e('Save Settings', 'bd-bookshelf'); ?></button> <button type="submit" class="button button-primary"><?php esc_html_e('Save Settings', 'umbra-bookshelf'); ?></button>
</p> </p>
</form> </form>
</div> </div>
@@ -725,23 +718,23 @@ class BDM_Admin {
*/ */
public function render_import_page() { public function render_import_page() {
if (!current_user_can('manage_options')) { if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf')); wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
} }
?> ?>
<div class="wrap"> <div class="wrap">
<h1><?php esc_html_e('Import Books', 'bd-bookshelf'); ?></h1> <h1><?php esc_html_e('Import Books', 'umbra-bookshelf'); ?></h1>
<div class="bd-bookshelf-import-section"> <div class="bd-bookshelf-import-section">
<h2><?php esc_html_e('Import from CSV', 'bd-bookshelf'); ?></h2> <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.', 'bd-bookshelf'); ?></p> <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"> <form id="bd-bookshelf-import-csv-form" enctype="multipart/form-data">
<?php wp_nonce_field('bd_bookshelf_admin_nonce', 'import_nonce'); ?> <?php wp_nonce_field('bd_bookshelf_admin_nonce', 'import_nonce'); ?>
<table class="form-table"> <table class="form-table">
<tr> <tr>
<th><label for="csv_file"><?php esc_html_e('CSV File', 'bd-bookshelf'); ?></label></th> <th><label for="csv_file"><?php esc_html_e('CSV File', 'umbra-bookshelf'); ?></label></th>
<td> <td>
<input type="file" name="csv_file" id="csv_file" accept=".csv" required> <input type="file" name="csv_file" id="csv_file" accept=".csv" required>
</td> </td>
@@ -749,36 +742,36 @@ class BDM_Admin {
</table> </table>
<p class="submit"> <p class="submit">
<button type="submit" class="button button-primary"><?php esc_html_e('Import CSV', 'bd-bookshelf'); ?></button> <button type="submit" class="button button-primary"><?php esc_html_e('Import CSV', 'umbra-bookshelf'); ?></button>
</p> </p>
</form> </form>
<div id="bd-bookshelf-import-results" style="display:none"> <div id="bd-bookshelf-import-results" style="display:none">
<h3><?php esc_html_e('Import Results', 'bd-bookshelf'); ?></h3> <h3><?php esc_html_e('Import Results', 'umbra-bookshelf'); ?></h3>
<p id="bd-bookshelf-import-message"></p> <p id="bd-bookshelf-import-message"></p>
</div> </div>
</div> </div>
<div class="bd-bookshelf-import-section"> <div class="bd-bookshelf-import-section">
<h2><?php esc_html_e('Import from WordPress Posts', 'bd-bookshelf'); ?></h2> <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.', 'bd-bookshelf'); ?></p> <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"> <form id="bd-bookshelf-import-posts-form">
<?php wp_nonce_field('bd_bookshelf_admin_nonce', 'import_posts_nonce'); ?> <?php wp_nonce_field('bd_bookshelf_admin_nonce', 'import_posts_nonce'); ?>
<table class="form-table"> <table class="form-table">
<tr> <tr>
<th><label for="post_type"><?php esc_html_e('Post Type', 'bd-bookshelf'); ?></label></th> <th><label for="post_type"><?php esc_html_e('Post Type', 'umbra-bookshelf'); ?></label></th>
<td> <td>
<select name="post_type" id="post_type"> <select name="post_type" id="post_type">
<option value="post"><?php esc_html_e('Posts', 'bd-bookshelf'); ?></option> <option value="post"><?php esc_html_e('Posts', 'umbra-bookshelf'); ?></option>
<option value="page"><?php esc_html_e('Pages', 'bd-bookshelf'); ?></option> <option value="page"><?php esc_html_e('Pages', 'umbra-bookshelf'); ?></option>
<option value="book"><?php esc_html_e('Books (if available)', 'bd-bookshelf'); ?></option> <option value="book"><?php esc_html_e('Books (if available)', 'umbra-bookshelf'); ?></option>
</select> </select>
</td> </td>
</tr> </tr>
<tr> <tr>
<th><label for="limit"><?php esc_html_e('Limit', 'bd-bookshelf'); ?></label></th> <th><label for="limit"><?php esc_html_e('Limit', 'umbra-bookshelf'); ?></label></th>
<td> <td>
<input type="number" name="limit" id="limit" value="100" min="1" max="1000"> <input type="number" name="limit" id="limit" value="100" min="1" max="1000">
</td> </td>
@@ -786,7 +779,7 @@ class BDM_Admin {
</table> </table>
<p class="submit"> <p class="submit">
<button type="submit" class="button button-primary"><?php esc_html_e('Import Posts', 'bd-bookshelf'); ?></button> <button type="submit" class="button button-primary"><?php esc_html_e('Import Posts', 'umbra-bookshelf'); ?></button>
</p> </p>
</form> </form>
</div> </div>
@@ -825,10 +818,10 @@ class BDM_Admin {
]; ];
if ($book_id) { if ($book_id) {
$result = BDM_Book_Manager::update_book($book_id, $data); $result = Umbra_Book_Manager::update_book($book_id, $data);
} else { } else {
$data['slug'] = sanitize_title($data['title']); $data['slug'] = sanitize_title($data['title']);
$book_id = BDM_Book_Manager::create_book($data); $book_id = Umbra_Book_Manager::create_book($data);
$result = $book_id !== false; $result = $book_id !== false;
} }
@@ -849,8 +842,8 @@ class BDM_Admin {
private function save_retail_urls($book_id, $retail_urls) { private function save_retail_urls($book_id, $retail_urls) {
global $wpdb; global $wpdb;
$links_table = BDM_Database::table('retail_links'); $links_table = Umbra_Database::table('retail_links');
$sites_table = BDM_Database::table('retail_sites'); $sites_table = Umbra_Database::table('retail_sites');
// Delete existing non-custom links // Delete existing non-custom links
$wpdb->delete($links_table, ['book_id' => $book_id, 'is_custom' => 0], ['%d', '%d']); $wpdb->delete($links_table, ['book_id' => $book_id, 'is_custom' => 0], ['%d', '%d']);
@@ -910,7 +903,7 @@ class BDM_Admin {
wp_send_json_error(['message' => 'Invalid book ID']); wp_send_json_error(['message' => 'Invalid book ID']);
} }
$result = BDM_Book_Manager::delete_book($book_id); $result = Umbra_Book_Manager::delete_book($book_id);
if ($result) { if ($result) {
wp_send_json_success(['message' => 'Book deleted']); wp_send_json_success(['message' => 'Book deleted']);
@@ -934,7 +927,7 @@ class BDM_Admin {
wp_send_json_error(['message' => 'Invalid author ID']); wp_send_json_error(['message' => 'Invalid author ID']);
} }
$author = BDM_Authors::get_author($author_id); $author = Umbra_Authors::get_author($author_id);
if (!$author) { if (!$author) {
wp_send_json_error(['message' => 'Author not found']); wp_send_json_error(['message' => 'Author not found']);
} }
@@ -964,9 +957,9 @@ class BDM_Admin {
]; ];
if ($author_id) { if ($author_id) {
$result = BDM_Authors::update_author($author_id, $data); $result = Umbra_Authors::update_author($author_id, $data);
} else { } else {
$author_id = BDM_Authors::create_author($data); $author_id = Umbra_Authors::create_author($data);
$result = $author_id !== false; $result = $author_id !== false;
} }
@@ -992,7 +985,7 @@ class BDM_Admin {
wp_send_json_error(['message' => 'Invalid author ID']); wp_send_json_error(['message' => 'Invalid author ID']);
} }
$result = BDM_Authors::delete_author($author_id); $result = Umbra_Authors::delete_author($author_id);
if ($result) { if ($result) {
wp_send_json_success(['message' => 'Author deleted']); wp_send_json_success(['message' => 'Author deleted']);
@@ -1016,7 +1009,7 @@ class BDM_Admin {
wp_send_json_error(['message' => 'Invalid series ID']); wp_send_json_error(['message' => 'Invalid series ID']);
} }
$series = BDM_Series::get_series_by_id($series_id); $series = Umbra_Series::get_series_by_id($series_id);
if (!$series) { if (!$series) {
wp_send_json_error(['message' => 'Series not found']); wp_send_json_error(['message' => 'Series not found']);
} }
@@ -1042,9 +1035,9 @@ class BDM_Admin {
]; ];
if ($series_id) { if ($series_id) {
$result = BDM_Series::update_series($series_id, $data); $result = Umbra_Series::update_series($series_id, $data);
} else { } else {
$series_id = BDM_Series::create_series($data); $series_id = Umbra_Series::create_series($data);
$result = $series_id !== false; $result = $series_id !== false;
} }
@@ -1070,7 +1063,7 @@ class BDM_Admin {
wp_send_json_error(['message' => 'Invalid series ID']); wp_send_json_error(['message' => 'Invalid series ID']);
} }
$result = BDM_Series::delete_series($series_id); $result = Umbra_Series::delete_series($series_id);
if ($result) { if ($result) {
wp_send_json_success(['message' => 'Series deleted']); wp_send_json_success(['message' => 'Series deleted']);
@@ -1095,7 +1088,7 @@ class BDM_Admin {
} }
global $wpdb; global $wpdb;
$table = BDM_Database::table('shortcodes'); $table = Umbra_Database::table('shortcodes');
$shortcode = $wpdb->get_row( $shortcode = $wpdb->get_row(
$wpdb->prepare( $wpdb->prepare(
"SELECT * FROM {$table} WHERE id = %d", "SELECT * FROM {$table} WHERE id = %d",
@@ -1134,9 +1127,9 @@ class BDM_Admin {
} }
if ($shortcode_id) { if ($shortcode_id) {
$result = BDM_Shortcodes::update_shortcode($shortcode_id, $data); $result = Umbra_Shortcodes::update_shortcode($shortcode_id, $data);
} else { } else {
$shortcode_id = BDM_Shortcodes::create_shortcode($data['slug'], $data['target_url'], $data['redirect_type'], $data['description']); $shortcode_id = Umbra_Shortcodes::create_shortcode($data['slug'], $data['target_url'], $data['redirect_type'], $data['description']);
$result = $shortcode_id !== false; $result = $shortcode_id !== false;
} }
@@ -1162,7 +1155,7 @@ class BDM_Admin {
wp_send_json_error(['message' => 'Invalid shortcode ID']); wp_send_json_error(['message' => 'Invalid shortcode ID']);
} }
$result = BDM_Shortcodes::delete_shortcode($shortcode_id); $result = Umbra_Shortcodes::delete_shortcode($shortcode_id);
if ($result) { if ($result) {
wp_send_json_success(['message' => 'Shortcode deleted']); wp_send_json_success(['message' => 'Shortcode deleted']);
@@ -1183,7 +1176,7 @@ class BDM_Admin {
if (!empty($_POST['affiliate'])) { if (!empty($_POST['affiliate'])) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('retail_sites'); $table = Umbra_Database::table('retail_sites');
foreach ($_POST['affiliate'] as $site_id => $code) { foreach ($_POST['affiliate'] as $site_id => $code) {
$wpdb->update( $wpdb->update(
@@ -1209,7 +1202,7 @@ class BDM_Admin {
wp_send_json_error(['message' => 'Insufficient permissions']); wp_send_json_error(['message' => 'Insufficient permissions']);
} }
BDM_GDPR::update_consent_text($_POST['gdpr_text'] ?? ''); Umbra_GDPR::update_consent_text($_POST['gdpr_text'] ?? '');
wp_send_json_success(['message' => 'Settings saved']); wp_send_json_success(['message' => 'Settings saved']);
} }
@@ -1252,7 +1245,7 @@ class BDM_Admin {
} }
// Check if already imported - update instead of skip // Check if already imported - update instead of skip
$existing = BDM_Book_Manager::get_book_by_title($data['title']); $existing = Umbra_Book_Manager::get_book_by_title($data['title']);
$book_data = [ $book_data = [
'title' => $data['title'], 'title' => $data['title'],
@@ -1276,14 +1269,14 @@ class BDM_Admin {
} }
} }
if (!empty($update_data)) { if (!empty($update_data)) {
BDM_Book_Manager::update_book($existing->id, $update_data); Umbra_Book_Manager::update_book($existing->id, $update_data);
$updated++; $updated++;
} else { } else {
$skipped++; $skipped++;
} }
} else { } else {
// Create new book // Create new book
$book_id = BDM_Book_Manager::create_book($book_data); $book_id = Umbra_Book_Manager::create_book($book_data);
if ($book_id) { if ($book_id) {
$imported++; $imported++;
} else { } else {
@@ -1332,14 +1325,14 @@ class BDM_Admin {
foreach ($posts as $post) { foreach ($posts as $post) {
// Check if already imported // Check if already imported
$existing = BDM_Book_Manager::get_book_by_title($post->post_title); $existing = Umbra_Book_Manager::get_book_by_title($post->post_title);
if ($existing) { if ($existing) {
$skipped++; $skipped++;
continue; continue;
} }
// Import book // Import book
$book_id = BDM_Book_Manager::create_book([ $book_id = Umbra_Book_Manager::create_book([
'title' => $post->post_title, 'title' => $post->post_title,
'description' => $post->post_content, 'description' => $post->post_content,
]); ]);
@@ -1368,7 +1361,7 @@ class BDM_Admin {
wp_send_json_error(['message' => 'Insufficient permissions']); wp_send_json_error(['message' => 'Insufficient permissions']);
} }
$result = BDM_Rating_Sync::manual_sync(); $result = Umbra_Rating_Sync::manual_sync();
wp_send_json_success([ wp_send_json_success([
'message' => sprintf('Synced %d books, failed %d', $result['synced'], $result['failed']), 'message' => sprintf('Synced %d books, failed %d', $result['synced'], $result['failed']),

View File

@@ -7,14 +7,14 @@
* *
* Fetches author and book data from Amazon * Fetches author and book data from Amazon
* *
* @package BD_Bookshelf * @package Umbra_Bookshelf
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
class BDM_Amazon_Scraper { class Umbra_Amazon_Scraper {
private $user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; private $user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';

View File

@@ -5,14 +5,14 @@
/** /**
* Authors Management class * Authors Management class
* *
* @package BD_Bookshelf * @package Umbra_Bookshelf
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
class BDM_Authors { class Umbra_Authors {
/** /**
* Get all authors * Get all authors
@@ -20,7 +20,7 @@ class BDM_Authors {
public static function get_authors($args = []) { public static function get_authors($args = []) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('authors'); $table = Umbra_Database::table('authors');
$defaults = [ $defaults = [
'number' => 20, 'number' => 20,
@@ -59,7 +59,7 @@ class BDM_Authors {
public static function get_author($author_id) { public static function get_author($author_id) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('authors'); $table = Umbra_Database::table('authors');
return $wpdb->get_row( return $wpdb->get_row(
$wpdb->prepare( $wpdb->prepare(
@@ -75,7 +75,7 @@ class BDM_Authors {
public static function create_author($data) { public static function create_author($data) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('authors'); $table = Umbra_Database::table('authors');
$result = $wpdb->insert($table, [ $result = $wpdb->insert($table, [
'display_name' => sanitize_text_field($data['display_name']), 'display_name' => sanitize_text_field($data['display_name']),
@@ -95,7 +95,7 @@ class BDM_Authors {
public static function update_author($author_id, $data) { public static function update_author($author_id, $data) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('authors'); $table = Umbra_Database::table('authors');
$update_data = []; $update_data = [];
@@ -145,10 +145,10 @@ class BDM_Authors {
global $wpdb; global $wpdb;
// Remove from book_authors // Remove from book_authors
$wpdb->delete(BDM_Database::table('book_authors'), ['author_id' => $author_id], ['%d']); $wpdb->delete(Umbra_Database::table('book_authors'), ['author_id' => $author_id], ['%d']);
// Delete author // Delete author
$table = BDM_Database::table('authors'); $table = Umbra_Database::table('authors');
return $wpdb->delete($table, ['id' => $author_id], ['%d']) !== false; return $wpdb->delete($table, ['id' => $author_id], ['%d']) !== false;
} }
@@ -158,7 +158,7 @@ class BDM_Authors {
public static function count_authors($search = '') { public static function count_authors($search = '') {
global $wpdb; global $wpdb;
$table = BDM_Database::table('authors'); $table = Umbra_Database::table('authors');
$where = '1=1'; $where = '1=1';
$params = []; $params = [];

View File

@@ -5,14 +5,14 @@
/** /**
* Book Manager class * Book Manager class
* *
* @package BD_Bookshelf * @package Umbra_Bookshelf
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
class BDM_Book_Manager { class Umbra_Book_Manager {
/** /**
* Get all books * Get all books
@@ -20,7 +20,7 @@ class BDM_Book_Manager {
public static function get_books($args = []) { public static function get_books($args = []) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('books'); $table = Umbra_Database::table('books');
$defaults = [ $defaults = [
'number' => 20, 'number' => 20,
@@ -63,7 +63,7 @@ class BDM_Book_Manager {
public static function get_book($book_id) { public static function get_book($book_id) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('books'); $table = Umbra_Database::table('books');
$book = $wpdb->get_row( $book = $wpdb->get_row(
$wpdb->prepare( $wpdb->prepare(
@@ -75,7 +75,7 @@ class BDM_Book_Manager {
if ($book) { if ($book) {
$book->authors = self::get_book_authors($book_id); $book->authors = self::get_book_authors($book_id);
$book->retail_links = self::get_book_retail_links($book_id); $book->retail_links = self::get_book_retail_links($book_id);
$book->seo = BDM_SEO::get_book_seo($book_id); $book->seo = Umbra_SEO::get_book_seo($book_id);
} }
return $book; return $book;
@@ -87,7 +87,7 @@ class BDM_Book_Manager {
public static function get_book_by_title($title) { public static function get_book_by_title($title) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('books'); $table = Umbra_Database::table('books');
$book = $wpdb->get_row( $book = $wpdb->get_row(
$wpdb->prepare( $wpdb->prepare(
@@ -105,7 +105,7 @@ class BDM_Book_Manager {
public static function get_book_by_slug($slug) { public static function get_book_by_slug($slug) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('books'); $table = Umbra_Database::table('books');
$book = $wpdb->get_row( $book = $wpdb->get_row(
$wpdb->prepare( $wpdb->prepare(
@@ -117,7 +117,7 @@ class BDM_Book_Manager {
if ($book) { if ($book) {
$book->authors = self::get_book_authors($book->id); $book->authors = self::get_book_authors($book->id);
$book->retail_links = self::get_book_retail_links($book->id); $book->retail_links = self::get_book_retail_links($book->id);
$book->seo = BDM_SEO::get_book_seo($book->id); $book->seo = Umbra_SEO::get_book_seo($book->id);
} }
return $book; return $book;
@@ -129,7 +129,7 @@ class BDM_Book_Manager {
public static function create_book($data) { public static function create_book($data) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('books'); $table = Umbra_Database::table('books');
$slug = sanitize_title($data['slug'] ?? $data['title'] ?? ''); $slug = sanitize_title($data['slug'] ?? $data['title'] ?? '');
@@ -184,7 +184,7 @@ class BDM_Book_Manager {
public static function update_book($book_id, $data) { public static function update_book($book_id, $data) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('books'); $table = Umbra_Database::table('books');
$update_data = []; $update_data = [];
@@ -274,12 +274,12 @@ class BDM_Book_Manager {
global $wpdb; global $wpdb;
// Delete related data // Delete related data
$wpdb->delete(BDM_Database::table('book_authors'), ['book_id' => $book_id], ['%d']); $wpdb->delete(Umbra_Database::table('book_authors'), ['book_id' => $book_id], ['%d']);
$wpdb->delete(BDM_Database::table('retail_links'), ['book_id' => $book_id], ['%d']); $wpdb->delete(Umbra_Database::table('retail_links'), ['book_id' => $book_id], ['%d']);
$wpdb->delete(BDM_Database::table('book_seo'), ['book_id' => $book_id], ['%d']); $wpdb->delete(Umbra_Database::table('book_seo'), ['book_id' => $book_id], ['%d']);
// Delete book // Delete book
$table = BDM_Database::table('books'); $table = Umbra_Database::table('books');
$result = $wpdb->delete($table, ['id' => $book_id], ['%d']); $result = $wpdb->delete($table, ['id' => $book_id], ['%d']);
return $result !== false; return $result !== false;
@@ -291,7 +291,7 @@ class BDM_Book_Manager {
public static function get_book_authors($book_id) { public static function get_book_authors($book_id) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('book_authors'); $table = Umbra_Database::table('book_authors');
$authors = $wpdb->get_results( $authors = $wpdb->get_results(
$wpdb->prepare( $wpdb->prepare(
@@ -309,8 +309,8 @@ class BDM_Book_Manager {
public static function update_book_authors_by_id($book_id, $author_ids) { public static function update_book_authors_by_id($book_id, $author_ids) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('book_authors'); $table = Umbra_Database::table('book_authors');
$authors_table = BDM_Database::table('authors'); $authors_table = Umbra_Database::table('authors');
// Delete existing authors // Delete existing authors
$wpdb->delete($table, ['book_id' => $book_id], ['%d']); $wpdb->delete($table, ['book_id' => $book_id], ['%d']);
@@ -349,7 +349,7 @@ class BDM_Book_Manager {
public static function update_book_authors($book_id, $authors) { public static function update_book_authors($book_id, $authors) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('book_authors'); $table = Umbra_Database::table('book_authors');
// Delete existing authors // Delete existing authors
$wpdb->delete($table, ['book_id' => $book_id], ['%d']); $wpdb->delete($table, ['book_id' => $book_id], ['%d']);
@@ -373,8 +373,8 @@ class BDM_Book_Manager {
public static function get_book_retail_links($book_id, $format = '') { public static function get_book_retail_links($book_id, $format = '') {
global $wpdb; global $wpdb;
$table = BDM_Database::table('retail_links'); $table = Umbra_Database::table('retail_links');
$sites_table = BDM_Database::table('retail_sites'); $sites_table = Umbra_Database::table('retail_sites');
$where = 'rl.book_id = %d AND rs.is_active = 1'; $where = 'rl.book_id = %d AND rs.is_active = 1';
$params = [$book_id]; $params = [$book_id];
@@ -404,7 +404,7 @@ class BDM_Book_Manager {
public static function get_books_by_series($series_id) { public static function get_books_by_series($series_id) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('books'); $table = Umbra_Database::table('books');
$books = $wpdb->get_results( $books = $wpdb->get_results(
$wpdb->prepare( $wpdb->prepare(
@@ -429,7 +429,7 @@ class BDM_Book_Manager {
public static function get_series_list() { public static function get_series_list() {
global $wpdb; global $wpdb;
$table = BDM_Database::table('series'); $table = Umbra_Database::table('series');
return $wpdb->get_results("SELECT * FROM {$table} ORDER BY name ASC"); return $wpdb->get_results("SELECT * FROM {$table} ORDER BY name ASC");
} }
@@ -440,7 +440,7 @@ class BDM_Book_Manager {
public static function count_books($search = '') { public static function count_books($search = '') {
global $wpdb; global $wpdb;
$table = BDM_Database::table('books'); $table = Umbra_Database::table('books');
$where = '1=1'; $where = '1=1';
$params = []; $params = [];

View File

@@ -5,14 +5,14 @@
/** /**
* Database management class * Database management class
* *
* @package BD_Bookshelf * @package Umbra_Bookshelf
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
class BDM_Database { class Umbra_Database {
/** /**
* Get table names with prefix * Get table names with prefix
@@ -187,7 +187,7 @@ class BDM_Database {
$this->insert_default_sites(); $this->insert_default_sites();
// Store schema version // Store schema version
update_option('bd_bookshelf_db_version', BD_BOOKSHELF_VERSION); update_option('bd_bookshelf_db_version', UMBRA_BOOKSHELF_VERSION);
} }
/** /**

View File

@@ -5,14 +5,14 @@
/** /**
* GDPR Compliance class * GDPR Compliance class
* *
* @package BD_Bookshelf * @package Umbra_Bookshelf
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
class BDM_GDPR { class Umbra_GDPR {
/** /**
* Constructor * Constructor
@@ -35,14 +35,14 @@ class BDM_GDPR {
} }
wp_enqueue_script( wp_enqueue_script(
'bd-bookshelf-gdpr', 'umbra-bookshelf-gdpr',
BD_BOOKSHELF_URL . 'public/js/bd-bookshelf-gdpr.js', UMBRA_BOOKSHELF_URL . 'public/js/umbra-bookshelf-gdpr.js',
[], [],
BD_BOOKSHELF_VERSION, UMBRA_BOOKSHELF_VERSION,
true true
); );
wp_localize_script('bd-bookshelf-gdpr', 'bdBookshelfGdpr', [ wp_localize_script('umbra-bookshelf-gdpr', 'umbraBookshelfGdpr', [
'ajaxurl' => admin_url('admin-ajax.php'), 'ajaxurl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('bd_bookshelf_gdpr_nonce'), 'nonce' => wp_create_nonce('bd_bookshelf_gdpr_nonce'),
'cookieDays' => 30, 'cookieDays' => 30,
@@ -69,7 +69,7 @@ class BDM_GDPR {
echo '<div id="bd-bookshelf-gdpr-banner" class="bd-bookshelf-gdpr-banner">'; echo '<div id="bd-bookshelf-gdpr-banner" class="bd-bookshelf-gdpr-banner">';
echo '<div class="bd-bookshelf-gdpr-content">'; echo '<div class="bd-bookshelf-gdpr-content">';
echo '<p>' . wp_kses_post($privacy_text) . '</p>'; echo '<p>' . wp_kses_post($privacy_text) . '</p>';
echo '<button id="bd-bookshelf-gdpr-accept" class="bd-bookshelf-gdpr-accept">' . esc_html__('Accept', 'bd-bookshelf') . '</button>'; echo '<button id="bd-bookshelf-gdpr-accept" class="bd-bookshelf-gdpr-accept">' . esc_html__('Accept', 'umbra-bookshelf') . '</button>';
echo '</div>'; echo '</div>';
echo '</div>'; echo '</div>';
} }

View File

@@ -7,14 +7,14 @@
* *
* Fetches author and book data from Goodreads * Fetches author and book data from Goodreads
* *
* @package BD_Bookshelf * @package Umbra_Bookshelf
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
class BDM_Goodreads_Scraper { class Umbra_Goodreads_Scraper {
private $user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; private $user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';

View File

@@ -5,39 +5,31 @@
/** /**
* Migration Manager class * Migration Manager class
* *
* @package BD_Bookshelf * @package Umbra_Bookshelf
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
class BDM_Migration { class Umbra_Migration {
/** /**
* Constructor * Constructor
*
* The "Import" admin page itself is registered once by Umbra_Admin
* (slug: umbra-bookshelf-import), which is the variant wired to
* admin/js/umbra-bookshelf-admin.js. This class only supplies the
* two-phase preview/run AJAX endpoints that page has no equivalent for.
*
* Note: the bd_bookshelf_import_csv action is intentionally NOT
* registered here -- Umbra_Admin already owns that action. Registering
* it twice silently overwrote one of the two handlers.
*/ */
public function __construct() { public function __construct() {
add_action('admin_menu', [$this, 'add_admin_menu']);
// AJAX handlers // AJAX handlers
add_action('wp_ajax_bd_bookshelf_preview_import', [$this, 'ajax_import_posts']); add_action('wp_ajax_bd_bookshelf_preview_import', [$this, 'ajax_import_posts']);
add_action('wp_ajax_bd_bookshelf_run_import', [$this, 'ajax_run_import_posts']); add_action('wp_ajax_bd_bookshelf_run_import', [$this, 'ajax_run_import_posts']);
add_action('wp_ajax_bd_bookshelf_import_csv', [$this, 'ajax_import_csv']);
}
/**
* Add admin menu
*/
public function add_admin_menu() {
add_submenu_page(
'bd-bookshelf',
__('Import Books', 'bd-bookshelf'),
__('Import', 'bd-bookshelf'),
'manage_options',
'bd-bookshelf-import',
[$this, 'render_import_page']
);
} }
/** /**
@@ -45,33 +37,33 @@ class BDM_Migration {
*/ */
public function render_import_page() { public function render_import_page() {
if (!current_user_can('manage_options')) { if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf')); wp_die(__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
} }
?> ?>
<div class="wrap"> <div class="wrap">
<h1><?php esc_html_e('Import Books', 'bd-bookshelf'); ?></h1> <h1><?php esc_html_e('Import Books', 'umbra-bookshelf'); ?></h1>
<div class="bd-bookshelf-import-section"> <div class="bd-bookshelf-import-section">
<h2><?php esc_html_e('Import from WordPress Posts', 'bd-bookshelf'); ?></h2> <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.', 'bd-bookshelf'); ?></p> <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"> <form id="bd-bookshelf-import-posts-form">
<?php wp_nonce_field('bd_bookshelf_import_posts', 'nonce'); ?> <?php wp_nonce_field('bd_bookshelf_import_posts', 'nonce'); ?>
<table class="form-table"> <table class="form-table">
<tr> <tr>
<th><label for="post_type"><?php esc_html_e('Post Type', 'bd-bookshelf'); ?></label></th> <th><label for="post_type"><?php esc_html_e('Post Type', 'umbra-bookshelf'); ?></label></th>
<td> <td>
<select name="post_type" id="post_type"> <select name="post_type" id="post_type">
<option value="post"><?php esc_html_e('Posts', 'bd-bookshelf'); ?></option> <option value="post"><?php esc_html_e('Posts', 'umbra-bookshelf'); ?></option>
<option value="page"><?php esc_html_e('Pages', 'bd-bookshelf'); ?></option> <option value="page"><?php esc_html_e('Pages', 'umbra-bookshelf'); ?></option>
<option value="book"><?php esc_html_e('Books (if available)', 'bd-bookshelf'); ?></option> <option value="book"><?php esc_html_e('Books (if available)', 'umbra-bookshelf'); ?></option>
</select> </select>
</td> </td>
</tr> </tr>
<tr> <tr>
<th><label for="limit"><?php esc_html_e('Limit', 'bd-bookshelf'); ?></label></th> <th><label for="limit"><?php esc_html_e('Limit', 'umbra-bookshelf'); ?></label></th>
<td> <td>
<input type="number" name="limit" id="limit" value="100" min="1" max="1000"> <input type="number" name="limit" id="limit" value="100" min="1" max="1000">
</td> </td>
@@ -79,38 +71,38 @@ class BDM_Migration {
</table> </table>
<p class="submit"> <p class="submit">
<button type="submit" class="button button-primary"><?php esc_html_e('Preview Import', 'bd-bookshelf'); ?></button> <button type="submit" class="button button-primary"><?php esc_html_e('Preview Import', 'umbra-bookshelf'); ?></button>
</p> </p>
</form> </form>
<div id="bd-bookshelf-import-preview" style="display: none;"> <div id="bd-bookshelf-import-preview" style="display: none;">
<h3><?php esc_html_e('Preview', 'bd-bookshelf'); ?></h3> <h3><?php esc_html_e('Preview', 'umbra-bookshelf'); ?></h3>
<div id="bd-bookshelf-import-results"></div> <div id="bd-bookshelf-import-results"></div>
<p class="submit"> <p class="submit">
<button type="button" id="bd-bookshelf-run-import" class="button button-primary"><?php esc_html_e('Run Import', 'bd-bookshelf'); ?></button> <button type="button" id="bd-bookshelf-run-import" class="button button-primary"><?php esc_html_e('Run Import', 'umbra-bookshelf'); ?></button>
</p> </p>
</div> </div>
</div> </div>
<div class="bd-bookshelf-import-section"> <div class="bd-bookshelf-import-section">
<h2><?php esc_html_e('Import from CSV', 'bd-bookshelf'); ?></h2> <h2><?php esc_html_e('Import from CSV', 'umbra-bookshelf'); ?></h2>
<p><?php esc_html_e('Import books from a CSV file.', 'bd-bookshelf'); ?></p> <p><?php esc_html_e('Import books from a CSV file.', 'umbra-bookshelf'); ?></p>
<form id="bd-bookshelf-import-csv-form" enctype="multipart/form-data"> <form id="bd-bookshelf-import-csv-form" enctype="multipart/form-data">
<?php wp_nonce_field('bd_bookshelf_import_csv', 'nonce'); ?> <?php wp_nonce_field('bd_bookshelf_import_csv', 'nonce'); ?>
<table class="form-table"> <table class="form-table">
<tr> <tr>
<th><label for="csv_file"><?php esc_html_e('CSV File', 'bd-bookshelf'); ?></label></th> <th><label for="csv_file"><?php esc_html_e('CSV File', 'umbra-bookshelf'); ?></label></th>
<td> <td>
<input type="file" name="csv_file" id="csv_file" accept=".csv"> <input type="file" name="csv_file" id="csv_file" accept=".csv">
<p class="description"><?php esc_html_e('Required columns: title. Optional: description, isbn_ebook, isbn_paperback, isbn_hardcover, isbn_audio, amazon_asin, goodreads_id.', 'bd-bookshelf'); ?></p> <p class="description"><?php esc_html_e('Required columns: title. Optional: description, isbn_ebook, isbn_paperback, isbn_hardcover, isbn_audio, amazon_asin, goodreads_id.', 'umbra-bookshelf'); ?></p>
</td> </td>
</tr> </tr>
</table> </table>
<p class="submit"> <p class="submit">
<button type="submit" class="button button-primary"><?php esc_html_e('Import CSV', 'bd-bookshelf'); ?></button> <button type="submit" class="button button-primary"><?php esc_html_e('Import CSV', 'umbra-bookshelf'); ?></button>
</p> </p>
</form> </form>
</div> </div>
@@ -174,14 +166,14 @@ class BDM_Migration {
foreach ($posts as $post) { foreach ($posts as $post) {
// Check if already imported // Check if already imported
$existing = BDM_Book_Manager::get_book_by_title($post->post_title); $existing = Umbra_Book_Manager::get_book_by_title($post->post_title);
if ($existing) { if ($existing) {
$skipped++; $skipped++;
continue; continue;
} }
// Import book // Import book
$book_id = BDM_Book_Manager::create_book([ $book_id = Umbra_Book_Manager::create_book([
'title' => $post->post_title, 'title' => $post->post_title,
'description' => $post->post_content, 'description' => $post->post_content,
]); ]);
@@ -237,14 +229,14 @@ class BDM_Migration {
} }
// Check if already imported // Check if already imported
$existing = BDM_Book_Manager::get_book_by_title($data['title']); $existing = Umbra_Book_Manager::get_book_by_title($data['title']);
if ($existing) { if ($existing) {
$skipped++; $skipped++;
continue; continue;
} }
// Create book // Create book
$book_id = BDM_Book_Manager::create_book([ $book_id = Umbra_Book_Manager::create_book([
'title' => $data['title'], 'title' => $data['title'],
'description' => $data['description'] ?? '', 'description' => $data['description'] ?? '',
'isbn_ebook' => $data['isbn_ebook'] ?? '', 'isbn_ebook' => $data['isbn_ebook'] ?? '',

View File

@@ -5,14 +5,14 @@
/** /**
* Rating Sync Engine class * Rating Sync Engine class
* *
* @package BD_Bookshelf * @package Umbra_Bookshelf
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
class BDM_Rating_Sync { class Umbra_Rating_Sync {
/** /**
* Constructor * Constructor
@@ -36,7 +36,7 @@ class BDM_Rating_Sync {
public function add_cron_interval($schedules) { public function add_cron_interval($schedules) {
$schedules['bd_bookshelf_twelve_hours'] = [ $schedules['bd_bookshelf_twelve_hours'] = [
'interval' => 43200, // 12 hours in seconds 'interval' => 43200, // 12 hours in seconds
'display' => esc_html__('Every 12 Hours', 'bd-bookshelf'), 'display' => esc_html__('Every 12 Hours', 'umbra-bookshelf'),
]; ];
return $schedules; return $schedules;
@@ -46,7 +46,7 @@ class BDM_Rating_Sync {
* Sync all book ratings * Sync all book ratings
*/ */
public function sync_all_ratings() { public function sync_all_ratings() {
$books = BDM_Book_Manager::get_books(['number' => 999]); $books = Umbra_Book_Manager::get_books(['number' => 999]);
$synced = 0; $synced = 0;
$failed = 0; $failed = 0;
@@ -81,7 +81,7 @@ class BDM_Rating_Sync {
public function sync_book_rating($book_id) { public function sync_book_rating($book_id) {
global $wpdb; global $wpdb;
$book = BDM_Book_Manager::get_book($book_id); $book = Umbra_Book_Manager::get_book($book_id);
if (!$book) { if (!$book) {
return false; return false;
} }
@@ -111,7 +111,7 @@ class BDM_Rating_Sync {
} }
// Update book ratings // Update book ratings
$table = BDM_Database::table('books'); $table = Umbra_Database::table('books');
$wpdb->update( $wpdb->update(
$table, $table,
[ [

View File

@@ -5,14 +5,14 @@
/** /**
* Retail Sites management class * Retail Sites management class
* *
* @package BD_Bookshelf * @package Umbra_Bookshelf
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
class BDM_Retail_Sites { class Umbra_Retail_Sites {
/** /**
* Get all retail sites * Get all retail sites
@@ -20,7 +20,7 @@ class BDM_Retail_Sites {
public static function get_sites($active_only = true) { public static function get_sites($active_only = true) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('retail_sites'); $table = Umbra_Database::table('retail_sites');
$where = $active_only ? 'is_active = 1' : '1=1'; $where = $active_only ? 'is_active = 1' : '1=1';
@@ -37,7 +37,7 @@ class BDM_Retail_Sites {
public static function get_site($site_id) { public static function get_site($site_id) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('retail_sites'); $table = Umbra_Database::table('retail_sites');
return $wpdb->get_row( return $wpdb->get_row(
$wpdb->prepare( $wpdb->prepare(
@@ -53,7 +53,7 @@ class BDM_Retail_Sites {
public static function create_site($data) { public static function create_site($data) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('retail_sites'); $table = Umbra_Database::table('retail_sites');
$result = $wpdb->insert($table, [ $result = $wpdb->insert($table, [
'name' => sanitize_text_field($data['name']), 'name' => sanitize_text_field($data['name']),
@@ -74,7 +74,7 @@ class BDM_Retail_Sites {
public static function update_site($site_id, $data) { public static function update_site($site_id, $data) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('retail_sites'); $table = Umbra_Database::table('retail_sites');
$update_data = []; $update_data = [];
@@ -126,8 +126,8 @@ class BDM_Retail_Sites {
// Only allow deleting custom sites // Only allow deleting custom sites
$site = self::get_site($site_id); $site = self::get_site($site_id);
if ($site && $site->is_custom) { if ($site && $site->is_custom) {
$wpdb->delete(BDM_Database::table('retail_links'), ['site_id' => $site_id], ['%d']); $wpdb->delete(Umbra_Database::table('retail_links'), ['site_id' => $site_id], ['%d']);
return $wpdb->delete(BDM_Database::table('retail_sites'), ['id' => $site_id], ['%d']) !== false; return $wpdb->delete(Umbra_Database::table('retail_sites'), ['id' => $site_id], ['%d']) !== false;
} }
return false; return false;
@@ -139,13 +139,13 @@ class BDM_Retail_Sites {
public static function generate_links_for_book($book_id) { public static function generate_links_for_book($book_id) {
global $wpdb; global $wpdb;
$book = BDM_Book_Manager::get_book($book_id); $book = Umbra_Book_Manager::get_book($book_id);
if (!$book) { if (!$book) {
return false; return false;
} }
$sites = self::get_sites(true); $sites = self::get_sites(true);
$links_table = BDM_Database::table('retail_links'); $links_table = Umbra_Database::table('retail_links');
// Delete existing links // Delete existing links
$wpdb->delete($links_table, ['book_id' => $book_id], ['%d']); $wpdb->delete($links_table, ['book_id' => $book_id], ['%d']);
@@ -266,7 +266,7 @@ class BDM_Retail_Sites {
public static function update_link($link_id, $url) { public static function update_link($link_id, $url) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('retail_links'); $table = Umbra_Database::table('retail_links');
return $wpdb->update( return $wpdb->update(
$table, $table,
@@ -283,7 +283,7 @@ class BDM_Retail_Sites {
public static function create_custom_link($book_id, $site_id, $format, $url) { public static function create_custom_link($book_id, $site_id, $format, $url) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('retail_links'); $table = Umbra_Database::table('retail_links');
$result = $wpdb->insert($table, [ $result = $wpdb->insert($table, [
'book_id' => $book_id, 'book_id' => $book_id,

View File

@@ -5,14 +5,14 @@
/** /**
* SEO Metadata Manager class * SEO Metadata Manager class
* *
* @package BD_Bookshelf * @package Umbra_Bookshelf
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
class BDM_SEO { class Umbra_SEO {
/** /**
* Constructor * Constructor
@@ -32,7 +32,7 @@ class BDM_SEO {
public static function get_book_seo($book_id) { public static function get_book_seo($book_id) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('book_seo'); $table = Umbra_Database::table('book_seo');
$seo = $wpdb->get_row( $seo = $wpdb->get_row(
$wpdb->prepare( $wpdb->prepare(
@@ -50,7 +50,7 @@ class BDM_SEO {
public static function update_book_seo($book_id, $data) { public static function update_book_seo($book_id, $data) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('book_seo'); $table = Umbra_Database::table('book_seo');
$existing = self::get_book_seo($book_id); $existing = self::get_book_seo($book_id);
@@ -85,7 +85,7 @@ class BDM_SEO {
public static function delete_book_seo($book_id) { public static function delete_book_seo($book_id) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('book_seo'); $table = Umbra_Database::table('book_seo');
return $wpdb->delete($table, ['book_id' => $book_id], ['%d']) !== false; return $wpdb->delete($table, ['book_id' => $book_id], ['%d']) !== false;
} }
@@ -138,7 +138,7 @@ class BDM_SEO {
} }
$seo = self::get_book_seo($book_id); $seo = self::get_book_seo($book_id);
$book = BDM_Book_Manager::get_book($book_id); $book = Umbra_Book_Manager::get_book($book_id);
if (!$book) { if (!$book) {
return; return;
@@ -206,7 +206,7 @@ class BDM_SEO {
return $entry; return $entry;
} }
$book = BDM_Book_Manager::get_book($book_id); $book = Umbra_Book_Manager::get_book($book_id);
if (!$book) { if (!$book) {
return $entry; return $entry;

View File

@@ -5,14 +5,14 @@
/** /**
* Series Management class * Series Management class
* *
* @package BD_Bookshelf * @package Umbra_Bookshelf
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
class BDM_Series { class Umbra_Series {
/** /**
* Get all series * Get all series
@@ -20,7 +20,7 @@ class BDM_Series {
public static function get_series($args = []) { public static function get_series($args = []) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('series'); $table = Umbra_Database::table('series');
$defaults = [ $defaults = [
'number' => 20, 'number' => 20,
@@ -48,7 +48,7 @@ class BDM_Series {
public static function get_series_by_id($series_id) { public static function get_series_by_id($series_id) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('series'); $table = Umbra_Database::table('series');
return $wpdb->get_row( return $wpdb->get_row(
$wpdb->prepare( $wpdb->prepare(
@@ -64,7 +64,7 @@ class BDM_Series {
public static function create_series($data) { public static function create_series($data) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('series'); $table = Umbra_Database::table('series');
$result = $wpdb->insert($table, [ $result = $wpdb->insert($table, [
'name' => sanitize_text_field($data['name']), 'name' => sanitize_text_field($data['name']),
@@ -81,7 +81,7 @@ class BDM_Series {
public static function update_series($series_id, $data) { public static function update_series($series_id, $data) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('series'); $table = Umbra_Database::table('series');
$update_data = []; $update_data = [];
@@ -119,7 +119,7 @@ class BDM_Series {
global $wpdb; global $wpdb;
// Unlink books from series // Unlink books from series
$books_table = BDM_Database::table('books'); $books_table = Umbra_Database::table('books');
$wpdb->update( $wpdb->update(
$books_table, $books_table,
['series_id' => 0], ['series_id' => 0],
@@ -129,7 +129,7 @@ class BDM_Series {
); );
// Delete series // Delete series
$table = BDM_Database::table('series'); $table = Umbra_Database::table('series');
return $wpdb->delete($table, ['id' => $series_id], ['%d']) !== false; return $wpdb->delete($table, ['id' => $series_id], ['%d']) !== false;
} }
@@ -139,7 +139,7 @@ class BDM_Series {
public static function count_series() { public static function count_series() {
global $wpdb; global $wpdb;
$table = BDM_Database::table('series'); $table = Umbra_Database::table('series');
return $wpdb->get_var("SELECT COUNT(*) FROM {$table}"); return $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
} }

View File

@@ -5,14 +5,14 @@
/** /**
* Shortcodes Manager class * Shortcodes Manager class
* *
* @package BD_Bookshelf * @package Umbra_Bookshelf
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
class BDM_Shortcodes { class Umbra_Shortcodes {
/** /**
* Constructor * Constructor
@@ -75,7 +75,7 @@ class BDM_Shortcodes {
return ''; return '';
} }
$links = BDM_Book_Manager::get_book_retail_links($book_id, $format); $links = Umbra_Book_Manager::get_book_retail_links($book_id, $format);
if (empty($links)) { if (empty($links)) {
return ''; return '';
} }
@@ -136,12 +136,12 @@ class BDM_Shortcodes {
return ''; return '';
} }
$book = BDM_Book_Manager::get_book_by_title(sanitize_text_field($atts['title'])); $book = Umbra_Book_Manager::get_book_by_title(sanitize_text_field($atts['title']));
if (!$book) { if (!$book) {
return ''; return '';
} }
$rating = BDM_Book_Manager::get_rating($book->id); $rating = Umbra_Book_Manager::get_rating($book->id);
$format = in_array($atts['format'], ['stars', 'numeric', 'both']) ? $atts['format'] : 'both'; $format = in_array($atts['format'], ['stars', 'numeric', 'both']) ? $atts['format'] : 'both';
$output = '<span class="bd-bookshelf-rating">'; $output = '<span class="bd-bookshelf-rating">';
@@ -201,7 +201,7 @@ class BDM_Shortcodes {
return; return;
} }
$table = BDM_Database::table('shortcodes'); $table = Umbra_Database::table('shortcodes');
$shortcode = $wpdb->get_row( $shortcode = $wpdb->get_row(
$wpdb->prepare( $wpdb->prepare(
"SELECT * FROM {$table} WHERE slug = %s AND is_active = 1", "SELECT * FROM {$table} WHERE slug = %s AND is_active = 1",
@@ -228,7 +228,7 @@ class BDM_Shortcodes {
private function track_click($shortcode_id) { private function track_click($shortcode_id) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('shortcode_clicks'); $table = Umbra_Database::table('shortcode_clicks');
$wpdb->insert($table, [ $wpdb->insert($table, [
'shortcode_id' => $shortcode_id, 'shortcode_id' => $shortcode_id,
'ip_address' => $this->get_client_ip(), 'ip_address' => $this->get_client_ip(),
@@ -239,7 +239,7 @@ class BDM_Shortcodes {
// Increment click count // Increment click count
$wpdb->query( $wpdb->query(
$wpdb->prepare( $wpdb->prepare(
"UPDATE " . BDM_Database::table('shortcodes') . " SET click_count = click_count + 1 WHERE id = %d", "UPDATE " . Umbra_Database::table('shortcodes') . " SET click_count = click_count + 1 WHERE id = %d",
$shortcode_id $shortcode_id
) )
); );
@@ -268,7 +268,7 @@ class BDM_Shortcodes {
public static function create_shortcode($slug, $target_url, $redirect_type = '301', $description = '') { public static function create_shortcode($slug, $target_url, $redirect_type = '301', $description = '') {
global $wpdb; global $wpdb;
$table = BDM_Database::table('shortcodes'); $table = Umbra_Database::table('shortcodes');
$result = $wpdb->insert($table, [ $result = $wpdb->insert($table, [
'slug' => sanitize_title($slug), 'slug' => sanitize_title($slug),
@@ -287,7 +287,7 @@ class BDM_Shortcodes {
public static function update_shortcode($shortcode_id, $data) { public static function update_shortcode($shortcode_id, $data) {
global $wpdb; global $wpdb;
$table = BDM_Database::table('shortcodes'); $table = Umbra_Database::table('shortcodes');
$update_data = []; $update_data = [];
@@ -332,7 +332,7 @@ class BDM_Shortcodes {
public static function delete_shortcode($shortcode_id) { public static function delete_shortcode($shortcode_id) {
global $wpdb; global $wpdb;
$wpdb->delete(BDM_Database::table('shortcode_clicks'), ['shortcode_id' => $shortcode_id], ['%d']); $wpdb->delete(Umbra_Database::table('shortcode_clicks'), ['shortcode_id' => $shortcode_id], ['%d']);
return $wpdb->delete(BDM_Database::table('shortcodes'), ['id' => $shortcode_id], ['%d']) !== false; return $wpdb->delete(Umbra_Database::table('shortcodes'), ['id' => $shortcode_id], ['%d']) !== false;
} }
} }

View File

@@ -5,10 +5,19 @@
/** /**
* Theme Compatibility Layer * Theme Compatibility Layer
* *
* Provides compatibility with BD BookHelper plugin interface * Provides the static helper interface the BD Bookshelf theme calls (book
* so the BD Bookshelf theme works with our new plugin. * covers, pen names, ratings, retailer links, series info, ...), backed by the
* catalogue module.
* *
* @package BD_Bookshelf * This class used to be declared as BD_BookShelfHelper, which collided with
* the library module's own BD_BookShelfHelper class. That collision was latent
* while the two were separate plugins -- it only fired when both were active --
* but became an unconditional "Cannot declare class" fatal error once they
* were merged into a single plugin. It now has its own name, and the
* class_alias() at the bottom keeps the legacy name working for the
* unmodified theme.
*
* @package Umbra_Bookshelf
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
@@ -16,10 +25,9 @@ if (!defined('ABSPATH')) {
} }
/** /**
* BD_BookShelfHelper class for theme compatibility * Static helper API consumed by the theme.
* Maps theme expectations to our new plugin methods
*/ */
class BD_BookShelfHelper { class Umbra_Theme_Compatibility {
/** /**
* Get book cover URL * Get book cover URL
@@ -77,7 +85,7 @@ class BD_BookShelfHelper {
return []; return [];
} }
$links = BDM_Book_Manager::get_book_retail_links($book->id, $format); $links = Umbra_Book_Manager::get_book_retail_links($book->id, $format);
$retailers = []; $retailers = [];
foreach ($links as $link) { foreach ($links as $link) {
@@ -102,7 +110,7 @@ class BD_BookShelfHelper {
* Get series book count * Get series book count
*/ */
public static function get_series_book_count($series_id) { public static function get_series_book_count($series_id) {
$books = BDM_Book_Manager::get_books_by_series($series_id); $books = Umbra_Book_Manager::get_books_by_series($series_id);
return count($books); return count($books);
} }
@@ -110,7 +118,7 @@ class BD_BookShelfHelper {
* Get series info * Get series info
*/ */
public static function get_series_info($series_id) { public static function get_series_info($series_id) {
$series = BDM_Series::get_series_by_id($series_id); $series = Umbra_Series::get_series_by_id($series_id);
if ($series) { if ($series) {
return [ return [
'name' => $series->name, 'name' => $series->name,
@@ -189,7 +197,7 @@ class BD_BookShelfHelper {
global $wpdb; global $wpdb;
// Check if post_id is actually a book ID from our database // Check if post_id is actually a book ID from our database
$book = BDM_Book_Manager::get_book($post_id); $book = Umbra_Book_Manager::get_book($post_id);
if ($book) { if ($book) {
return $book; return $book;
} }
@@ -198,13 +206,13 @@ class BD_BookShelfHelper {
$book_id = get_post_meta($post_id, '_bd_bookshelf_book_id', true); $book_id = get_post_meta($post_id, '_bd_bookshelf_book_id', true);
if ($book_id) { if ($book_id) {
return BDM_Book_Manager::get_book($book_id); return Umbra_Book_Manager::get_book($book_id);
} }
// Try to find by title // Try to find by title
$post = get_post($post_id); $post = get_post($post_id);
if ($post) { if ($post) {
$book = BDM_Book_Manager::get_book_by_title($post->post_title); $book = Umbra_Book_Manager::get_book_by_title($post->post_title);
if ($book) { if ($book) {
// Link post to book // Link post to book
update_post_meta($post_id, '_bd_bookshelf_book_id', $book->id); update_post_meta($post_id, '_bd_bookshelf_book_id', $book->id);
@@ -215,3 +223,14 @@ class BD_BookShelfHelper {
return null; return null;
} }
} }
/**
* Legacy alias.
*
* The BD Bookshelf theme (unchanged, and out of scope for this consolidation)
* still calls BD_BookShelfHelper:: statically. Keep that working. Guarded
* because a theme or add-on may legitimately have declared the name already.
*/
if (!class_exists('BD_BookShelfHelper', false)) {
class_alias('Umbra_Theme_Compatibility', 'BD_BookShelfHelper');
}

View File

@@ -5,20 +5,20 @@
/** /**
* Email Admin Class * Email Admin Class
* *
* @package BD_SimpleEmail * @package Umbra_Simple_Email
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
class BD_SimpleEmail_Admin { class Umbra_Simple_Email_Admin {
private static $instance = null; private static $instance = null;
/** /**
* Get instance * Get instance
* *
* @return BD_SimpleEmail_Admin * @return Umbra_Simple_Email_Admin
*/ */
public static function instance() { public static function instance() {
if (null === self::$instance) { if (null === self::$instance) {
@@ -40,19 +40,23 @@ class BD_SimpleEmail_Admin {
* @param string $hook Current admin page. * @param string $hook Current admin page.
*/ */
public function enqueue_assets($hook) { public function enqueue_assets($hook) {
if (strpos($hook, 'bd-simpleemail') !== false || strpos($hook, 'bd-email') !== false) { // Matches the submenu slugs registered by Umbra_Simple_Email::add_menu_pages().
if (strpos($hook, 'umbra-bookshelf-subscribers') !== false
|| strpos($hook, 'umbra-bookshelf-newsletter') !== false
|| strpos($hook, 'umbra-bookshelf-email-settings') !== false) {
wp_enqueue_style( wp_enqueue_style(
'bd-email-admin', 'umbra-bookshelf-email-admin',
BD_EMAIL_URI . 'assets/css/admin.css', UMBRA_BOOKSHELF_URL . 'email/assets/css/admin.css',
array(), array(),
BD_EMAIL_VERSION UMBRA_BOOKSHELF_EMAIL_VERSION
); );
wp_enqueue_script( wp_enqueue_script(
'bd-email-admin', 'umbra-bookshelf-email-admin',
BD_EMAIL_URI . 'assets/js/admin.js', UMBRA_BOOKSHELF_URL . 'email/assets/js/admin.js',
array('jquery'), array('jquery'),
BD_EMAIL_VERSION, UMBRA_BOOKSHELF_EMAIL_VERSION,
true true
); );
} }
@@ -143,7 +147,7 @@ class BD_SimpleEmail_Admin {
} }
if ($content) { if ($content) {
$result = BD_Email_Sender::send_newsletter($subject, $content); $result = Umbra_Email_Sender::send_newsletter($subject, $content);
if ($result['success']) { if ($result['success']) {
self::log_newsletter($subject, $content, $result['count']); self::log_newsletter($subject, $content, $result['count']);
@@ -163,7 +167,7 @@ class BD_SimpleEmail_Admin {
} }
} }
$settings = BD_SimpleEmail::get_settings(); $settings = Umbra_Simple_Email::get_settings();
$has_ai = !empty($settings['gemini_api_key']); $has_ai = !empty($settings['gemini_api_key']);
echo '<div class="wrap">'; echo '<div class="wrap">';
@@ -231,7 +235,7 @@ class BD_SimpleEmail_Admin {
update_option('bdm_reply_to', sanitize_email($_POST['reply_to'] ?? '')); update_option('bdm_reply_to', sanitize_email($_POST['reply_to'] ?? ''));
if (!empty($_POST['gmail_password'])) { if (!empty($_POST['gmail_password'])) {
update_option('bdm_gmail_password', BD_Email_Sender::encrypt_password($_POST['gmail_password'])); update_option('bdm_gmail_password', Umbra_Email_Sender::encrypt_password($_POST['gmail_password']));
} }
if (!empty($_POST['gemini_api_key'])) { if (!empty($_POST['gemini_api_key'])) {
@@ -241,8 +245,8 @@ class BD_SimpleEmail_Admin {
echo '<div class="updated"><p>' . esc_html__('Settings saved.', 'bd-simpleemail') . '</p></div>'; echo '<div class="updated"><p>' . esc_html__('Settings saved.', 'bd-simpleemail') . '</p></div>';
} }
$settings = BD_SimpleEmail::get_settings(); $settings = Umbra_Simple_Email::get_settings();
$business = BD_SimpleEmail::get_business_info(); $business = Umbra_Simple_Email::get_business_info();
echo '<div class="wrap">'; echo '<div class="wrap">';
echo '<h1>' . esc_html__('BD SimpleEmail Settings', 'bd-simpleemail') . '</h1>'; echo '<h1>' . esc_html__('BD SimpleEmail Settings', 'bd-simpleemail') . '</h1>';
@@ -305,7 +309,7 @@ class BD_SimpleEmail_Admin {
* @return string * @return string
*/ */
private static function generate_newsletter($subject, $prompt) { private static function generate_newsletter($subject, $prompt) {
$settings = BD_SimpleEmail::get_settings(); $settings = Umbra_Simple_Email::get_settings();
if (empty($settings['gemini_api_key'])) { if (empty($settings['gemini_api_key'])) {
return ''; return '';

View File

@@ -5,14 +5,14 @@
/** /**
* Email Installation and Database * Email Installation and Database
* *
* @package BD_SimpleEmail * @package Umbra_Simple_Email
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
class BD_SimpleEmail_Install { class Umbra_Simple_Email_Install {
/** /**
* Activate plugin * Activate plugin
*/ */

View File

@@ -5,14 +5,14 @@
/** /**
* Email Sender Class * Email Sender Class
* *
* @package BD_SimpleEmail * @package Umbra_Simple_Email
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
class BD_Email_Sender { class Umbra_Email_Sender {
/** /**
* Send newsletter to all subscribers * Send newsletter to all subscribers
* *
@@ -21,25 +21,25 @@ class BD_Email_Sender {
* @return array * @return array
*/ */
public static function send_newsletter($subject, $content) { public static function send_newsletter($subject, $content) {
$settings = BD_SimpleEmail::get_settings(); $settings = Umbra_Simple_Email::get_settings();
if (empty($settings['gmail_address']) || empty($settings['gmail_password'])) { if (empty($settings['gmail_address']) || empty($settings['gmail_password'])) {
return array( return array(
'success' => false, 'success' => false,
'message' => esc_html__('Gmail not configured', 'bd-simpleemail'), 'message' => esc_html__('Gmail not configured', 'umbra-bookshelf'),
); );
} }
$subscribers = BD_SimpleEmail::get_subscribers(); $subscribers = Umbra_Simple_Email::get_subscribers();
if (empty($subscribers)) { if (empty($subscribers)) {
return array( return array(
'success' => false, 'success' => false,
'message' => esc_html__('No subscribers found', 'bd-simpleemail'), 'message' => esc_html__('No subscribers found', 'umbra-bookshelf'),
); );
} }
$business = BD_SimpleEmail::get_business_info(); $business = Umbra_Simple_Email::get_business_info();
$html = self::build_email_html($subject, $content, $business); $html = self::build_email_html($subject, $content, $business);
$sent = 0; $sent = 0;
@@ -75,10 +75,10 @@ class BD_Email_Sender {
* @return bool * @return bool
*/ */
private static function send_single_email($to, $subject, $html, $first_name = '') { private static function send_single_email($to, $subject, $html, $first_name = '') {
$settings = BD_SimpleEmail::get_settings(); $settings = Umbra_Simple_Email::get_settings();
// Replace placeholder with actual unsubscribe link // Replace placeholder with actual unsubscribe link
$token = BD_SimpleEmail::generate_unsubscribe_token($to); $token = Umbra_Simple_Email::generate_unsubscribe_token($to);
$unsubscribe_url = add_query_arg(array( $unsubscribe_url = add_query_arg(array(
'email' => rawurlencode($to), 'email' => rawurlencode($to),
'token' => $token, 'token' => $token,

View File

@@ -5,20 +5,20 @@
/** /**
* Main SimpleEmail Plugin Class * Main SimpleEmail Plugin Class
* *
* @package BD_SimpleEmail * @package Umbra_Simple_Email
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
class BD_SimpleEmail { class Umbra_Simple_Email {
private static $instance = null; private static $instance = null;
/** /**
* Get instance * Get instance
* *
* @return BD_SimpleEmail * @return Umbra_Simple_Email
*/ */
public static function instance() { public static function instance() {
if (null === self::$instance) { if (null === self::$instance) {
@@ -51,41 +51,34 @@ class BD_SimpleEmail {
* Add admin menu pages * Add admin menu pages
*/ */
public function add_menu_pages() { public function add_menu_pages() {
add_menu_page( // The top-level "Umbra Bookshelf" menu is registered once by the plugin
esc_html__('BD SimpleEmail', 'bd-simpleemail'), // bootstrap (Umbra_Bookshelf::register_admin_menu); this module only
esc_html__('BD SimpleEmail', 'bd-simpleemail'), // contributes submenus beneath it.
add_submenu_page(
'umbra-bookshelf',
esc_html__('Subscribers', 'umbra-bookshelf'),
esc_html__('Subscribers', 'umbra-bookshelf'),
'manage_options', 'manage_options',
'bd-simpleemail', 'umbra-bookshelf-subscribers',
'', array('Umbra_Simple_Email_Admin', 'subscribers_page')
'dashicons-email',
32
); );
add_submenu_page( add_submenu_page(
'bd-simpleemail', 'umbra-bookshelf',
esc_html__('Subscribers', 'bd-simpleemail'), esc_html__('New Newsletter', 'umbra-bookshelf'),
esc_html__('Subscribers', 'bd-simpleemail'), esc_html__('New Newsletter', 'umbra-bookshelf'),
'manage_options', 'manage_options',
'bd-email-subscribers', 'umbra-bookshelf-newsletter',
array('BD_SimpleEmail_Admin', 'subscribers_page') array('Umbra_Simple_Email_Admin', 'new_newsletter_page')
); );
add_submenu_page( add_submenu_page(
'bd-simpleemail', 'umbra-bookshelf',
esc_html__('New Newsletter', 'bd-simpleemail'), esc_html__('Email Settings', 'umbra-bookshelf'),
esc_html__('New Newsletter', 'bd-simpleemail'), esc_html__('Email Settings', 'umbra-bookshelf'),
'manage_options', 'manage_options',
'bd-email-new', 'umbra-bookshelf-email-settings',
array('BD_SimpleEmail_Admin', 'new_newsletter_page') array('Umbra_Simple_Email_Admin', 'settings_page')
);
add_submenu_page(
'bd-simpleemail',
esc_html__('Settings', 'bd-simpleemail'),
esc_html__('Settings', 'bd-simpleemail'),
'manage_options',
'bd-email-settings',
array('BD_SimpleEmail_Admin', 'settings_page')
); );
} }
@@ -145,22 +138,22 @@ class BD_SimpleEmail {
$email = sanitize_email($_POST['email'] ?? ''); $email = sanitize_email($_POST['email'] ?? '');
if (!$email || !is_email($email)) { if (!$email || !is_email($email)) {
wp_send_json_error(array('message' => esc_html__('Invalid email address', 'bd-simpleemail'))); wp_send_json_error(array('message' => esc_html__('Invalid email address', 'umbra-bookshelf')));
} }
$first_name = sanitize_text_field($_POST['first_name'] ?? ''); $first_name = sanitize_text_field($_POST['first_name'] ?? '');
$opt_in = isset($_POST['opt_in']) && $_POST['opt_in']; $opt_in = isset($_POST['opt_in']) && $_POST['opt_in'];
if (!$opt_in) { if (!$opt_in) {
wp_send_json_error(array('message' => esc_html__('Please confirm your subscription', 'bd-simpleemail'))); wp_send_json_error(array('message' => esc_html__('Please confirm your subscription', 'umbra-bookshelf')));
} }
$result = self::subscribe($email, $first_name); $result = self::subscribe($email, $first_name);
if ($result) { if ($result) {
wp_send_json_success(array('message' => esc_html__('Thank you for subscribing!', 'bd-simpleemail'))); wp_send_json_success(array('message' => esc_html__('Thank you for subscribing!', 'umbra-bookshelf')));
} else { } else {
wp_send_json_error(array('message' => esc_html__('You are already subscribed or cannot be added', 'bd-simpleemail'))); wp_send_json_error(array('message' => esc_html__('You are already subscribed or cannot be added', 'umbra-bookshelf')));
} }
} }
@@ -316,19 +309,19 @@ class BD_SimpleEmail {
<form id="bdm-signup-form" class="bdm-signup-form"> <form id="bdm-signup-form" class="bdm-signup-form">
<?php wp_nonce_field('bdm_ajax_signup', 'bdm_signup_nonce'); ?> <?php wp_nonce_field('bdm_ajax_signup', 'bdm_signup_nonce'); ?>
<p class="form-field"> <p class="form-field">
<label for="bdm-email"><?php esc_html_e('Email Address', 'bd-simpleemail'); ?> *</label> <label for="bdm-email"><?php esc_html_e('Email Address', 'umbra-bookshelf'); ?> *</label>
<input type="email" id="bdm-email" name="email" required> <input type="email" id="bdm-email" name="email" required>
</p> </p>
<p class="form-field"> <p class="form-field">
<label for="bdm-first-name"><?php esc_html_e('First Name', 'bd-simpleemail'); ?></label> <label for="bdm-first-name"><?php esc_html_e('First Name', 'umbra-bookshelf'); ?></label>
<input type="text" id="bdm-first-name" name="first_name"> <input type="text" id="bdm-first-name" name="first_name">
</p> </p>
<p class="form-field checkbox-field"> <p class="form-field checkbox-field">
<input type="checkbox" id="bdm-opt-in" name="opt_in" value="1" required> <input type="checkbox" id="bdm-opt-in" name="opt_in" value="1" required>
<label for="bdm-opt-in"><?php esc_html_e('I agree to receive email updates.', 'bd-simpleemail'); ?></label> <label for="bdm-opt-in"><?php esc_html_e('I agree to receive email updates.', 'umbra-bookshelf'); ?></label>
</p> </p>
<p class="form-submit"> <p class="form-submit">
<button type="submit" class="submit-btn"><?php esc_html_e('Subscribe', 'bd-simpleemail'); ?></button> <button type="submit" class="submit-btn"><?php esc_html_e('Subscribe', 'umbra-bookshelf'); ?></button>
</p> </p>
<div class="form-response" style="display:none;"></div> <div class="form-response" style="display:none;"></div>
</form> </form>
@@ -360,7 +353,7 @@ class BD_SimpleEmail {
response.style.display = 'block'; response.style.display = 'block';
}) })
.catch(function() { .catch(function() {
response.innerHTML = '<p class="error"><?php esc_html_e('An error occurred. Please try again.', 'bd-simpleemail'); ?></p>'; response.innerHTML = '<p class="error"><?php esc_html_e('An error occurred. Please try again.', 'umbra-bookshelf'); ?></p>';
response.style.display = 'block'; response.style.display = 'block';
}); });
}); });

View File

@@ -5,20 +5,20 @@
/** /**
* Main Plugin Class * Main Plugin Class
* *
* @package BD_BookShelfHelper * @package Umbra_Bookshelf_Helper
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
class BD_BookShelfHelper { class Umbra_Bookshelf_Helper {
private static $instance = null; private static $instance = null;
/** /**
* Get instance * Get instance
* *
* @return BD_BookShelfHelper * @return Umbra_Bookshelf_Helper
*/ */
public static function instance() { public static function instance() {
if (null === self::$instance) { if (null === self::$instance) {
@@ -48,7 +48,7 @@ class BD_BookShelfHelper {
add_filter('bdm_nav_menu_items', array($this, 'filter_series_menu_item'), 10, 2); add_filter('bdm_nav_menu_items', array($this, 'filter_series_menu_item'), 10, 2);
if (is_admin()) { if (is_admin()) {
BD_BookShelfHelper_Admin::instance(); Umbra_Bookshelf_Helper_Admin::instance();
} }
// Schedule cron jobs on init // Schedule cron jobs on init
@@ -65,20 +65,20 @@ class BD_BookShelfHelper {
public function register_post_types() { public function register_post_types() {
register_post_type('book', array( register_post_type('book', array(
'labels' => array( 'labels' => array(
'name' => esc_html__('Books', 'bd-bookshelfhelper'), 'name' => esc_html__('Books', 'umbra-bookshelf'),
'singular_name' => esc_html__('Book', 'bd-bookshelfhelper'), 'singular_name' => esc_html__('Book', 'umbra-bookshelf'),
'add_new' => esc_html__('Add New', 'bd-bookshelfhelper'), 'add_new' => esc_html__('Add New', 'umbra-bookshelf'),
'add_new_item' => esc_html__('Add New Book', 'bd-bookshelfhelper'), 'add_new_item' => esc_html__('Add New Book', 'umbra-bookshelf'),
'edit_item' => esc_html__('Edit Book', 'bd-bookshelfhelper'), 'edit_item' => esc_html__('Edit Book', 'umbra-bookshelf'),
'new_item' => esc_html__('New Book', 'bd-bookshelfhelper'), 'new_item' => esc_html__('New Book', 'umbra-bookshelf'),
'view_item' => esc_html__('View Book', 'bd-bookshelfhelper'), 'view_item' => esc_html__('View Book', 'umbra-bookshelf'),
'search_items' => esc_html__('Search Books', 'bd-bookshelfhelper'), 'search_items' => esc_html__('Search Books', 'umbra-bookshelf'),
'not_found' => esc_html__('No books found', 'bd-bookshelfhelper'), 'not_found' => esc_html__('No books found', 'umbra-bookshelf'),
'not_found_in_trash' => esc_html__('No books found in trash', 'bd-bookshelfhelper'), 'not_found_in_trash' => esc_html__('No books found in trash', 'umbra-bookshelf'),
), ),
'public' => true, 'public' => true,
'has_archive' => true, 'has_archive' => true,
'show_in_menu' => 'bd-bookshelf', 'show_in_menu' => 'umbra-bookshelf',
'supports' => array('title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'), 'supports' => array('title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'),
'rewrite' => array('slug' => 'book'), 'rewrite' => array('slug' => 'book'),
'menu_icon' => 'dashicons-book-alt', 'menu_icon' => 'dashicons-book-alt',
@@ -94,15 +94,15 @@ class BD_BookShelfHelper {
public function register_taxonomies() { public function register_taxonomies() {
register_taxonomy('series', 'book', array( register_taxonomy('series', 'book', array(
'labels' => array( 'labels' => array(
'name' => esc_html__('Series', 'bd-bookshelfhelper'), 'name' => esc_html__('Series', 'umbra-bookshelf'),
'singular_name' => esc_html__('Series', 'bd-bookshelfhelper'), 'singular_name' => esc_html__('Series', 'umbra-bookshelf'),
'search_items' => esc_html__('Search Series', 'bd-bookshelfhelper'), 'search_items' => esc_html__('Search Series', 'umbra-bookshelf'),
'all_items' => esc_html__('All Series', 'bd-bookshelfhelper'), 'all_items' => esc_html__('All Series', 'umbra-bookshelf'),
'edit_item' => esc_html__('Edit Series', 'bd-bookshelfhelper'), 'edit_item' => esc_html__('Edit Series', 'umbra-bookshelf'),
'update_item' => esc_html__('Update Series', 'bd-bookshelfhelper'), 'update_item' => esc_html__('Update Series', 'umbra-bookshelf'),
'add_new_item' => esc_html__('Add New Series', 'bd-bookshelfhelper'), 'add_new_item' => esc_html__('Add New Series', 'umbra-bookshelf'),
'new_item_name' => esc_html__('New Series Name', 'bd-bookshelfhelper'), 'new_item_name' => esc_html__('New Series Name', 'umbra-bookshelf'),
'not_found' => esc_html__('No series found', 'bd-bookshelfhelper'), 'not_found' => esc_html__('No series found', 'umbra-bookshelf'),
), ),
'hierarchical' => true, 'hierarchical' => true,
'public' => true, 'public' => true,
@@ -157,7 +157,7 @@ class BD_BookShelfHelper {
$series_count = self::series_count(); $series_count = self::series_count();
if ($series_count > 0) { if ($series_count > 0) {
$series_item = '<li class="menu-item menu-item-type-taxonomy series-menu-item"><a href="' . home_url('/series/') . '">' . esc_html__('Series', 'bd-bookshelfhelper') . '</a></li>'; $series_item = '<li class="menu-item menu-item-type-taxonomy series-menu-item"><a href="' . home_url('/series/') . '">' . esc_html__('Series', 'umbra-bookshelf') . '</a></li>';
if (preg_match('/(<li[^>]*>.*?Books.*?<\/li>)/s', $items, $matches)) { if (preg_match('/(<li[^>]*>.*?Books.*?<\/li>)/s', $items, $matches)) {
$books_item = $matches[1]; $books_item = $matches[1];
@@ -273,10 +273,10 @@ class BD_BookShelfHelper {
* @return string * @return string
*/ */
private function render_book_display($book) { private function render_book_display($book) {
$cover_url = BD_BookShelfHelper::get_cover_url($book->ID, 'medium'); $cover_url = Umbra_Bookshelf_Helper::get_cover_url($book->ID, 'medium');
$pen_name = BD_BookShelfHelper::get_pen_name($book->ID); $pen_name = Umbra_Bookshelf_Helper::get_pen_name($book->ID);
$blurb = BD_BookShelfHelper::get_localized_blurb($book->ID); $blurb = Umbra_Bookshelf_Helper::get_localized_blurb($book->ID);
$links = BD_BookShelfHelper::get_retailer_links($book->ID); $links = Umbra_Bookshelf_Helper::get_retailer_links($book->ID);
ob_start(); ob_start();
?> ?>
@@ -508,7 +508,7 @@ class BD_BookShelfHelper {
} }
} }
return BD_BHS_URI . 'assets/images/default-cover.png'; return UMBRA_BOOKSHELF_URL . 'assets/images/default-cover.png';
} }
/** /**

View File

@@ -5,20 +5,20 @@
/** /**
* Admin Class * Admin Class
* *
* @package BD_BookShelfHelper * @package Umbra_Bookshelf_Helper
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
class BD_BookShelfHelper_Admin { class Umbra_Bookshelf_Helper_Admin {
private static $instance = null; private static $instance = null;
/** /**
* Get instance * Get instance
* *
* @return BD_BookShelfHelper_Admin * @return Umbra_Bookshelf_Helper_Admin
*/ */
public static function instance() { public static function instance() {
if (null === self::$instance) { if (null === self::$instance) {
@@ -43,73 +43,68 @@ class BD_BookShelfHelper_Admin {
* Add admin menu pages * Add admin menu pages
*/ */
public function add_menu_pages() { public function add_menu_pages() {
add_menu_page( // The top-level "Umbra Bookshelf" menu is registered once by the plugin
esc_html__('BD BookHelper', 'bd-bookshelfhelper'), // bootstrap (Umbra_Bookshelf::register_admin_menu); this module only
esc_html__('BD BookHelper', 'bd-bookshelfhelper'), // contributes submenus beneath it.
'edit_posts',
'bd-bookshelf',
'',
'dashicons-book-alt',
30
);
add_submenu_page( add_submenu_page(
'bd-bookshelf', 'umbra-bookshelf',
esc_html__('Books', 'bd-bookshelfhelper'), esc_html__('Books', 'umbra-bookshelf'),
esc_html__('All Books', 'bd-bookshelfhelper'), esc_html__('Book Library', 'umbra-bookshelf'),
'edit_posts', 'edit_posts',
'edit.php?post_type=book' 'edit.php?post_type=book'
); );
add_submenu_page( add_submenu_page(
'bd-bookshelf', 'umbra-bookshelf',
esc_html__('Add Book', 'bd-bookshelfhelper'), esc_html__('Add Book', 'umbra-bookshelf'),
esc_html__('Add Book', 'bd-bookshelfhelper'), esc_html__('Add Book', 'umbra-bookshelf'),
'edit_posts', 'edit_posts',
'post-new.php?post_type=book' 'post-new.php?post_type=book'
); );
add_submenu_page( add_submenu_page(
'bd-bookshelf', 'umbra-bookshelf',
esc_html__('Book Data Entry', 'bd-bookshelfhelper'), esc_html__('Book Data Entry', 'umbra-bookshelf'),
esc_html__('Data Entry', 'bd-bookshelfhelper'), esc_html__('Data Entry', 'umbra-bookshelf'),
'edit_posts', 'edit_posts',
'bd-bookshelf-data-entry', 'umbra-bookshelf-data-entry',
array($this, 'data_entry_page') array($this, 'data_entry_page')
); );
add_submenu_page( add_submenu_page(
'bd-bookshelf', 'umbra-bookshelf',
esc_html__('Series', 'bd-bookshelfhelper'), esc_html__('Series', 'umbra-bookshelf'),
esc_html__('Series', 'bd-bookshelfhelper'), esc_html__('Series Terms', 'umbra-bookshelf'),
'manage_categories', 'manage_categories',
'edit-tags.php?taxonomy=series&post_type=book' 'edit-tags.php?taxonomy=series&post_type=book'
); );
add_submenu_page( add_submenu_page(
'bd-bookshelf', 'umbra-bookshelf',
esc_html__('Pen Names', 'bd-bookshelfhelper'), esc_html__('Pen Names', 'umbra-bookshelf'),
esc_html__('Pen Names', 'bd-bookshelfhelper'), esc_html__('Pen Names', 'umbra-bookshelf'),
'manage_options', 'manage_options',
'bd-bookshelf-pen-names', 'umbra-bookshelf-pen-names',
array($this, 'pen_names_page') array($this, 'pen_names_page')
); );
// Distinct slug from Umbra_Admin's "umbra-bookshelf-settings": these are
// two different settings screens (library/publishing vs. catalogue).
add_submenu_page( add_submenu_page(
'bd-bookshelf', 'umbra-bookshelf',
esc_html__('Settings', 'bd-bookshelfhelper'), esc_html__('Library Settings', 'umbra-bookshelf'),
esc_html__('Settings', 'bd-bookshelfhelper'), esc_html__('Library Settings', 'umbra-bookshelf'),
'manage_options', 'manage_options',
'bd-bookshelf-settings', 'umbra-bookshelf-library-settings',
array($this, 'settings_page') array($this, 'settings_page')
); );
add_submenu_page( add_submenu_page(
'bd-bookshelf', 'umbra-bookshelf',
esc_html__('Migrate BD Books', 'bd-bookshelfhelper'), esc_html__('Migrate Books', 'umbra-bookshelf'),
esc_html__('Migrate', 'bd-bookshelfhelper'), esc_html__('Migrate', 'umbra-bookshelf'),
'manage_options', 'manage_options',
'bd-bookshelf-migrate', 'umbra-bookshelf-migrate',
array($this, 'migrate_page') array($this, 'migrate_page')
); );
} }
@@ -126,23 +121,40 @@ class BD_BookShelfHelper_Admin {
wp_enqueue_media(); wp_enqueue_media();
} }
if (strpos($hook, 'bd-bookshelf') !== false) { // Scoped to this module's own submenu slugs. A bare strpos('umbra-bookshelf')
// would also match every books-module and email-module screen.
$library_hooks = array(
'umbra-bookshelf-data-entry',
'umbra-bookshelf-pen-names',
'umbra-bookshelf-library-settings',
'umbra-bookshelf-migrate',
);
$is_library_screen = false;
foreach ($library_hooks as $library_hook) {
if (strpos($hook, $library_hook) !== false) {
$is_library_screen = true;
break;
}
}
if ($is_library_screen) {
wp_enqueue_style( wp_enqueue_style(
'bd-bookshelf-admin', 'umbra-bookshelf-library-admin',
BD_BHS_URI . 'assets/css/admin.css', UMBRA_BOOKSHELF_URL . 'library/assets/css/admin.css',
array(), array(),
BD_BHS_VERSION UMBRA_BOOKSHELF_LIBRARY_VERSION
); );
wp_enqueue_script( wp_enqueue_script(
'bd-bookshelf-admin', 'umbra-bookshelf-library-admin',
BD_BHS_URI . 'assets/js/admin.js', UMBRA_BOOKSHELF_URL . 'library/assets/js/admin.js',
array('jquery'), array('jquery'),
BD_BHS_VERSION, UMBRA_BOOKSHELF_LIBRARY_VERSION,
true true
); );
wp_localize_script('bd-bookshelf-admin', 'bdmAdmin', array( wp_localize_script('umbra-bookshelf-library-admin', 'umbraBookshelfLibraryAdmin', array(
'ajaxUrl' => admin_url('admin-ajax.php'), 'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('bd_bookshelf_admin'), 'nonce' => wp_create_nonce('bd_bookshelf_admin'),
)); ));
@@ -155,7 +167,7 @@ class BD_BookShelfHelper_Admin {
public function add_meta_boxes() { public function add_meta_boxes() {
add_meta_box( add_meta_box(
'bd_book_details', 'bd_book_details',
esc_html__('Book Details', 'bd-bookshelfhelper'), esc_html__('Book Details', 'umbra-bookshelf'),
array($this, 'book_details_meta_box'), array($this, 'book_details_meta_box'),
'book', 'book',
'normal', 'normal',
@@ -164,7 +176,7 @@ class BD_BookShelfHelper_Admin {
add_meta_box( add_meta_box(
'bd_book_seo', 'bd_book_seo',
esc_html__('SEO Settings', 'bd-bookshelfhelper'), esc_html__('SEO Settings', 'umbra-bookshelf'),
array($this, 'book_seo_meta_box'), array($this, 'book_seo_meta_box'),
'book', 'book',
'side', 'side',
@@ -178,19 +190,19 @@ class BD_BookShelfHelper_Admin {
* @param WP_Post $post Post object. * @param WP_Post $post Post object.
*/ */
public function book_details_meta_box($post) { public function book_details_meta_box($post) {
$languages = BD_BookShelfHelper::get_supported_languages(); $languages = Umbra_Bookshelf_Helper::get_supported_languages();
wp_nonce_field('bd_book_meta', 'bd_book_nonce'); wp_nonce_field('bd_book_meta', 'bd_book_nonce');
echo '<div class="bd-book-meta-box">'; echo '<div class="bd-book-meta-box">';
// Core Fields // Core Fields
echo '<h3>' . esc_html__('Core Fields', 'bd-bookshelfhelper') . '</h3>'; echo '<h3>' . esc_html__('Core Fields', 'umbra-bookshelf') . '</h3>';
// Pen Name // Pen Name
echo '<p><label>' . esc_html__('Pen Name', 'bd-bookshelfhelper') . '</label>'; echo '<p><label>' . esc_html__('Pen Name', 'umbra-bookshelf') . '</label>';
echo '<select name="pen_name_id">'; echo '<select name="pen_name_id">';
echo '<option value="">' . esc_html__('B.D. Murphy (Default)', 'bd-bookshelfhelper') . '</option>'; echo '<option value="">' . esc_html__('B.D. Murphy (Default)', 'umbra-bookshelf') . '</option>';
global $wpdb; global $wpdb;
$table_name = $wpdb->prefix . 'bd_pen_names'; $table_name = $wpdb->prefix . 'bd_pen_names';
@@ -205,24 +217,24 @@ class BD_BookShelfHelper_Admin {
echo '</select></p>'; echo '</select></p>';
// ASIN and ISBN // ASIN and ISBN
echo '<p><label>' . esc_html__('ASIN (Amazon)', 'bd-bookshelfhelper') . '</label>'; echo '<p><label>' . esc_html__('ASIN (Amazon)', 'umbra-bookshelf') . '</label>';
echo '<input type="text" name="asin" value="' . esc_attr(get_post_meta($post->ID, 'asin', true)) . '" class="regular-text"></p>'; echo '<input type="text" name="asin" value="' . esc_attr(get_post_meta($post->ID, 'asin', true)) . '" class="regular-text"></p>';
echo '<p><label>' . esc_html__('ISBN-13 (Print)', 'bd-bookshelfhelper') . '</label>'; echo '<p><label>' . esc_html__('ISBN-13 (Print)', 'umbra-bookshelf') . '</label>';
echo '<input type="text" name="isbn" value="' . esc_attr(get_post_meta($post->ID, 'isbn', true)) . '" class="regular-text"></p>'; echo '<input type="text" name="isbn" value="' . esc_attr(get_post_meta($post->ID, 'isbn', true)) . '" class="regular-text"></p>';
// Short Link // Short Link
echo '<p><label>' . esc_html__('Short Link Slug', 'bd-bookshelfhelper') . '</label>'; echo '<p><label>' . esc_html__('Short Link Slug', 'umbra-bookshelf') . '</label>';
echo '<input type="text" name="short_link_slug" value="' . esc_attr(get_post_meta($post->ID, 'short_link_slug', true)) . '" class="regular-text">'; echo '<input type="text" name="short_link_slug" value="' . esc_attr(get_post_meta($post->ID, 'short_link_slug', true)) . '" class="regular-text">';
echo '<p class="description">' . esc_html__('Auto-generated if empty. E.g., "ph" for "Pandemic Hacker"', 'bd-bookshelfhelper') . '</p></p>'; echo '<p class="description">' . esc_html__('Auto-generated if empty. E.g., "ph" for "Pandemic Hacker"', 'umbra-bookshelf') . '</p></p>';
// Series // Series
$series_terms = get_terms(array('taxonomy' => 'series', 'hide_empty' => false)); $series_terms = get_terms(array('taxonomy' => 'series', 'hide_empty' => false));
if ($series_terms) { if ($series_terms) {
echo '<p><label>' . esc_html__('Series', 'bd-bookshelfhelper') . '</label>'; echo '<p><label>' . esc_html__('Series', 'umbra-bookshelf') . '</label>';
echo '<select name="series_id">'; echo '<select name="series_id">';
echo '<option value="">' . esc_html__('None / Standalone', 'bd-bookshelfhelper') . '</option>'; echo '<option value="">' . esc_html__('None / Standalone', 'umbra-bookshelf') . '</option>';
$current_series = get_the_terms($post->ID, 'series'); $current_series = get_the_terms($post->ID, 'series');
$current_series_id = ($current_series && !is_wp_error($current_series)) ? $current_series[0]->term_id : 0; $current_series_id = ($current_series && !is_wp_error($current_series)) ? $current_series[0]->term_id : 0;
@@ -233,33 +245,33 @@ class BD_BookShelfHelper_Admin {
echo '</select></p>'; echo '</select></p>';
if ($current_series_id) { if ($current_series_id) {
echo '<p><label>' . esc_html__('Volume Number', 'bd-bookshelfhelper') . '</label>'; echo '<p><label>' . esc_html__('Volume Number', 'umbra-bookshelf') . '</label>';
echo '<input type="number" name="volume_number" value="' . esc_attr(get_post_meta($post->ID, 'volume_number', true)) . '" min="1"></p>'; echo '<input type="number" name="volume_number" value="' . esc_attr(get_post_meta($post->ID, 'volume_number', true)) . '" min="1"></p>';
} }
} }
// Language Content // Language Content
echo '<h3>' . esc_html__('Language Content', 'bd-bookshelfhelper') . '</h3>'; echo '<h3>' . esc_html__('Language Content', 'umbra-bookshelf') . '</h3>';
foreach ($languages as $code => $name) { foreach ($languages as $code => $name) {
echo '<fieldset class="bd-lang-fieldset">'; echo '<fieldset class="bd-lang-fieldset">';
echo '<legend>' . esc_html($name) . ' (' . strtoupper($code) . ')</legend>'; echo '<legend>' . esc_html($name) . ' (' . strtoupper($code) . ')</legend>';
echo '<p><label>' . esc_html__('Title', 'bd-bookshelfhelper') . '</label>'; echo '<p><label>' . esc_html__('Title', 'umbra-bookshelf') . '</label>';
echo '<input type="text" name="title_' . esc_attr($code) . '" value="' . esc_attr(get_post_meta($post->ID, 'title_' . $code, true)) . '" class="large-text"></p>'; echo '<input type="text" name="title_' . esc_attr($code) . '" value="' . esc_attr(get_post_meta($post->ID, 'title_' . $code, true)) . '" class="large-text"></p>';
echo '<p><label>' . esc_html__('Back Cover Blurb', 'bd-bookshelfhelper') . '</label>'; echo '<p><label>' . esc_html__('Back Cover Blurb', 'umbra-bookshelf') . '</label>';
echo '<textarea name="blurb_' . esc_attr($code) . '" rows="4" class="large-text">' . esc_textarea(get_post_meta($post->ID, 'blurb_' . $code, true)) . '</textarea></p>'; echo '<textarea name="blurb_' . esc_attr($code) . '" rows="4" class="large-text">' . esc_textarea(get_post_meta($post->ID, 'blurb_' . $code, true)) . '</textarea></p>';
echo '<p><label>' . esc_html__('Preview Excerpt', 'bd-bookshelfhelper') . '</label>'; echo '<p><label>' . esc_html__('Preview Excerpt', 'umbra-bookshelf') . '</label>';
echo '<textarea name="preview_excerpt_' . esc_attr($code) . '" rows="3" class="large-text">' . esc_textarea(get_post_meta($post->ID, 'preview_excerpt_' . $code, true)) . '</textarea></p>'; echo '<textarea name="preview_excerpt_' . esc_attr($code) . '" rows="3" class="large-text">' . esc_textarea(get_post_meta($post->ID, 'preview_excerpt_' . $code, true)) . '</textarea></p>';
echo '<p><label>' . esc_html__('Pull Quotes (1-5)', 'bd-bookshelfhelper') . '</label>'; echo '<p><label>' . esc_html__('Pull Quotes (1-5)', 'umbra-bookshelf') . '</label>';
for ($i = 1; $i <= 5; $i++) { for ($i = 1; $i <= 5; $i++) {
printf('<input type="text" name="pull_quote_%s_%d" value="%s" class="large-text" placeholder="%s">', printf('<input type="text" name="pull_quote_%s_%d" value="%s" class="large-text" placeholder="%s">',
esc_attr($code), $i, esc_attr($code), $i,
esc_attr(get_post_meta($post->ID, 'pull_quote_' . $code . '_' . $i, true)), esc_attr(get_post_meta($post->ID, 'pull_quote_' . $code . '_' . $i, true)),
esc_attr__('Quote ' . $i, 'bd-bookshelfhelper') esc_attr__('Quote ' . $i, 'umbra-bookshelf')
); );
} }
echo '</p>'; echo '</p>';
@@ -268,7 +280,7 @@ class BD_BookShelfHelper_Admin {
} }
// Retailer Links // Retailer Links
echo '<h3>' . esc_html__('Retailer Links', 'bd-bookshelfhelper') . '</h3>'; echo '<h3>' . esc_html__('Retailer Links', 'umbra-bookshelf') . '</h3>';
$retailer_fields = array( $retailer_fields = array(
'ebook_url_amazon' => 'Amazon (Ebook)', 'ebook_url_amazon' => 'Amazon (Ebook)',
@@ -287,12 +299,12 @@ class BD_BookShelfHelper_Admin {
} }
// Ratings // Ratings
echo '<h3>' . esc_html__('Ratings', 'bd-bookshelfhelper') . '</h3>'; echo '<h3>' . esc_html__('Ratings', 'umbra-bookshelf') . '</h3>';
echo '<p><label>' . esc_html__('Amazon Rating', 'bd-bookshelfhelper') . '</label>'; echo '<p><label>' . esc_html__('Amazon Rating', 'umbra-bookshelf') . '</label>';
echo '<input type="text" name="amazon_rating" value="' . esc_attr(get_post_meta($post->ID, 'amazon_rating', true)) . '" placeholder="4.5"></p>'; echo '<input type="text" name="amazon_rating" value="' . esc_attr(get_post_meta($post->ID, 'amazon_rating', true)) . '" placeholder="4.5"></p>';
echo '<p><label>' . esc_html__('Goodreads Rating', 'bd-bookshelfhelper') . '</label>'; echo '<p><label>' . esc_html__('Goodreads Rating', 'umbra-bookshelf') . '</label>';
echo '<input type="text" name="goodreads_rating" value="' . esc_attr(get_post_meta($post->ID, 'goodreads_rating', true)) . '" placeholder="4.2"></p>'; echo '<input type="text" name="goodreads_rating" value="' . esc_attr(get_post_meta($post->ID, 'goodreads_rating', true)) . '" placeholder="4.2"></p>';
echo '</div>'; echo '</div>';
@@ -304,10 +316,10 @@ class BD_BookShelfHelper_Admin {
* @param WP_Post $post Post object. * @param WP_Post $post Post object.
*/ */
public function book_seo_meta_box($post) { public function book_seo_meta_box($post) {
echo '<p><label>' . esc_html__('SEO Title', 'bd-bookshelfhelper') . '</label>'; echo '<p><label>' . esc_html__('SEO Title', 'umbra-bookshelf') . '</label>';
echo '<input type="text" name="seo_title" value="' . esc_attr(get_post_meta($post->ID, 'seo_title', true)) . '" class="large-text"></p>'; echo '<input type="text" name="seo_title" value="' . esc_attr(get_post_meta($post->ID, 'seo_title', true)) . '" class="large-text"></p>';
echo '<p><label>' . esc_html__('SEO Description', 'bd-bookshelfhelper') . '</label>'; echo '<p><label>' . esc_html__('SEO Description', 'umbra-bookshelf') . '</label>';
echo '<textarea name="seo_description" rows="4" class="large-text">' . esc_textarea(get_post_meta($post->ID, 'seo_description', true)) . '</textarea></p>'; echo '<textarea name="seo_description" rows="4" class="large-text">' . esc_textarea(get_post_meta($post->ID, 'seo_description', true)) . '</textarea></p>';
} }
@@ -316,7 +328,7 @@ class BD_BookShelfHelper_Admin {
*/ */
public function data_entry_page() { public function data_entry_page() {
$post_id = isset($_GET['post_id']) ? absint($_GET['post_id']) : 0; $post_id = isset($_GET['post_id']) ? absint($_GET['post_id']) : 0;
$languages = BD_BookShelfHelper::get_supported_languages(); $languages = Umbra_Bookshelf_Helper::get_supported_languages();
// Get all books for selection // Get all books for selection
global $wpdb; global $wpdb;
@@ -327,9 +339,9 @@ class BD_BookShelfHelper_Admin {
// If no post_id provided and no books exist, show selection // If no post_id provided and no books exist, show selection
if (!$post_id && empty($books)) { if (!$post_id && empty($books)) {
echo '<div class="wrap">'; echo '<div class="wrap">';
echo '<h1>' . esc_html__('Book Data Entry', 'bd-bookshelfhelper') . '</h1>'; echo '<h1>' . esc_html__('Book Data Entry', 'umbra-bookshelf') . '</h1>';
echo '<div class="bd-status-message warning">' . esc_html__('No books found. Please create a book first.', 'bd-bookshelfhelper') . '</div>'; echo '<div class="bd-status-message warning">' . esc_html__('No books found. Please create a book first.', 'umbra-bookshelf') . '</div>';
echo '<p><a href="' . esc_url(admin_url('post-new.php?post_type=book')) . '" class="button button-primary">' . esc_html__('Create New Book', 'bd-bookshelfhelper') . '</a></p>'; echo '<p><a href="' . esc_url(admin_url('post-new.php?post_type=book')) . '" class="button button-primary">' . esc_html__('Create New Book', 'umbra-bookshelf') . '</a></p>';
echo '</div>'; echo '</div>';
return; return;
} }
@@ -342,18 +354,18 @@ class BD_BookShelfHelper_Admin {
// If still no post_id, show selection // If still no post_id, show selection
if (!$post_id) { if (!$post_id) {
echo '<div class="wrap">'; echo '<div class="wrap">';
echo '<h1>' . esc_html__('Book Data Entry', 'bd-bookshelfhelper') . '</h1>'; echo '<h1>' . esc_html__('Book Data Entry', 'umbra-bookshelf') . '</h1>';
echo '<form method="post">'; echo '<form method="post">';
echo '<table class="form-table">'; echo '<table class="form-table">';
echo '<tr><th>' . esc_html__('Select Book', 'bd-bookshelfhelper') . '</th>'; echo '<tr><th>' . esc_html__('Select Book', 'umbra-bookshelf') . '</th>';
echo '<td><select name="selected_book_id">'; echo '<td><select name="selected_book_id">';
echo '<option value="">' . esc_html__('Select a book...', 'bd-bookshelfhelper') . '</option>'; echo '<option value="">' . esc_html__('Select a book...', 'umbra-bookshelf') . '</option>';
foreach ($books as $book) { foreach ($books as $book) {
echo '<option value="' . esc_attr($book->ID) . '">' . esc_html($book->post_title) . '</option>'; echo '<option value="' . esc_attr($book->ID) . '">' . esc_html($book->post_title) . '</option>';
} }
echo '</select></td></tr>'; echo '</select></td></tr>';
echo '</table>'; echo '</table>';
submit_button(esc_html__('Select Book', 'bd-bookshelfhelper'), 'primary', 'bd_select_book'); submit_button(esc_html__('Select Book', 'umbra-bookshelf'), 'primary', 'bd_select_book');
echo '</form>'; echo '</form>';
echo '</div>'; echo '</div>';
return; return;
@@ -362,7 +374,7 @@ class BD_BookShelfHelper_Admin {
$post = get_post($post_id); $post = get_post($post_id);
if (!$post || 'book' !== $post->post_type) { if (!$post || 'book' !== $post->post_type) {
echo '<div class="wrap">'; echo '<div class="wrap">';
echo '<div class="bd-status-message error">' . esc_html__('Invalid book selected.', 'bd-bookshelfhelper') . '</div>'; echo '<div class="bd-status-message error">' . esc_html__('Invalid book selected.', 'umbra-bookshelf') . '</div>';
echo '</div>'; echo '</div>';
return; return;
} }
@@ -370,7 +382,7 @@ class BD_BookShelfHelper_Admin {
wp_nonce_field('bd_data_entry', 'bd_data_entry_nonce'); wp_nonce_field('bd_data_entry', 'bd_data_entry_nonce');
echo '<div class="wrap">'; echo '<div class="wrap">';
echo '<h1>' . sprintf(esc_html__('Data Entry: %s', 'bd-bookshelfhelper'), esc_html($post->post_title)) . '</h1>'; echo '<h1>' . sprintf(esc_html__('Data Entry: %s', 'umbra-bookshelf'), esc_html($post->post_title)) . '</h1>';
// Language tabs // Language tabs
echo '<div class="bd-language-tabs">'; echo '<div class="bd-language-tabs">';
@@ -389,20 +401,20 @@ class BD_BookShelfHelper_Admin {
wp_nonce_field('bd_data_entry_save', 'bd_data_entry_save_nonce'); wp_nonce_field('bd_data_entry_save', 'bd_data_entry_save_nonce');
// Section 1: Basic Information // Section 1: Basic Information
$this->render_section_header('basic', __('Basic Information', 'bd-bookshelfhelper'), true); $this->render_section_header('basic', __('Basic Information', 'umbra-bookshelf'), true);
echo '<div class="bd-section-content" id="bd-section-basic">'; echo '<div class="bd-section-content" id="bd-section-basic">';
echo '<div class="bd-field-row">'; echo '<div class="bd-field-row">';
echo '<div class="bd-field required">'; echo '<div class="bd-field required">';
echo '<label for="post_title">' . esc_html__('Book Title', 'bd-bookshelfhelper') . '</label>'; echo '<label for="post_title">' . esc_html__('Book Title', 'umbra-bookshelf') . '</label>';
echo '<input type="text" id="post_title" name="post_title" value="' . esc_attr($post->post_title) . '" class="large-text" required>'; echo '<input type="text" id="post_title" name="post_title" value="' . esc_attr($post->post_title) . '" class="large-text" required>';
echo '<span class="bd-hint">' . esc_html__('The full title of the book as it appears on the cover.', 'bd-bookshelfhelper') . '</span>'; echo '<span class="bd-hint">' . esc_html__('The full title of the book as it appears on the cover.', 'umbra-bookshelf') . '</span>';
echo '</div>'; echo '</div>';
echo '<div class="bd-field">'; echo '<div class="bd-field">';
echo '<label for="pen_name_id">' . esc_html__('Pen Name', 'bd-bookshelfhelper') . '</label>'; echo '<label for="pen_name_id">' . esc_html__('Pen Name', 'umbra-bookshelf') . '</label>';
echo '<select id="pen_name_id" name="pen_name_id">'; echo '<select id="pen_name_id" name="pen_name_id">';
echo '<option value="">' . esc_html__('B.D. Murphy (Default)', 'bd-bookshelfhelper') . '</option>'; echo '<option value="">' . esc_html__('B.D. Murphy (Default)', 'umbra-bookshelf') . '</option>';
$pen_names = $wpdb->get_results("SELECT id, pen_name FROM {$wpdb->prefix}bd_pen_names WHERE active = 1 ORDER BY pen_name"); $pen_names = $wpdb->get_results("SELECT id, pen_name FROM {$wpdb->prefix}bd_pen_names WHERE active = 1 ORDER BY pen_name");
$pen_name_id = get_post_meta($post_id, 'pen_name_id', true); $pen_name_id = get_post_meta($post_id, 'pen_name_id', true);
@@ -413,55 +425,55 @@ class BD_BookShelfHelper_Admin {
} }
} }
echo '</select>'; echo '</select>';
echo '<span class="bd-hint">' . esc_html__('The author name to display with this book.', 'bd-bookshelfhelper') . '</span>'; echo '<span class="bd-hint">' . esc_html__('The author name to display with this book.', 'umbra-bookshelf') . '</span>';
echo '</div>'; echo '</div>';
echo '</div>'; echo '</div>';
echo '<div class="bd-field-row">'; echo '<div class="bd-field-row">';
echo '<div class="bd-field">'; echo '<div class="bd-field">';
echo '<label for="publish_date">' . esc_html__('Publication Date', 'bd-bookshelfhelper') . '</label>'; echo '<label for="publish_date">' . esc_html__('Publication Date', 'umbra-bookshelf') . '</label>';
echo '<input type="date" id="publish_date" name="publish_date" value="' . esc_attr(get_post_meta($post_id, 'publish_date', true)) . '">'; echo '<input type="date" id="publish_date" name="publish_date" value="' . esc_attr(get_post_meta($post_id, 'publish_date', true)) . '">';
echo '<span class="bd-hint">' . esc_html__('Format: YYYY-MM-DD (e.g., 2024-01-15)', 'bd-bookshelfhelper') . '</span>'; echo '<span class="bd-hint">' . esc_html__('Format: YYYY-MM-DD (e.g., 2024-01-15)', 'umbra-bookshelf') . '</span>';
echo '</div>'; echo '</div>';
echo '<div class="bd-field">'; echo '<div class="bd-field">';
echo '<label for="sort_order">' . esc_html__('Sort Order', 'bd-bookshelfhelper') . '</label>'; echo '<label for="sort_order">' . esc_html__('Sort Order', 'umbra-bookshelf') . '</label>';
echo '<input type="number" id="sort_order" name="sort_order" value="' . esc_attr(get_post_meta($post_id, 'sort_order', true)) . '" min="0">'; echo '<input type="number" id="sort_order" name="sort_order" value="' . esc_attr(get_post_meta($post_id, 'sort_order', true)) . '" min="0">';
echo '<span class="bd-hint">' . esc_html__('Manual sort position (lower numbers appear first).', 'bd-bookshelfhelper') . '</span>'; echo '<span class="bd-hint">' . esc_html__('Manual sort position (lower numbers appear first).', 'umbra-bookshelf') . '</span>';
echo '</div>'; echo '</div>';
echo '</div>'; echo '</div>';
echo '</div>'; // End Basic Information echo '</div>'; // End Basic Information
// Section 2: Identifiers // Section 2: Identifiers
$this->render_section_header('identifiers', __('Identifiers', 'bd-bookshelfhelper'), true); $this->render_section_header('identifiers', __('Identifiers', 'umbra-bookshelf'), true);
echo '<div class="bd-section-content" id="bd-section-identifiers">'; echo '<div class="bd-section-content" id="bd-section-identifiers">';
echo '<div class="bd-field-row">'; echo '<div class="bd-field-row">';
echo '<div class="bd-field">'; echo '<div class="bd-field">';
echo '<label for="isbn">' . esc_html__('ISBN-13 (Print)', 'bd-bookshelfhelper') . '</label>'; echo '<label for="isbn">' . esc_html__('ISBN-13 (Print)', 'umbra-bookshelf') . '</label>';
echo '<input type="text" id="isbn" name="isbn" value="' . esc_attr(get_post_meta($post_id, 'isbn', true)) . '" placeholder="9781234567890">'; echo '<input type="text" id="isbn" name="isbn" value="' . esc_attr(get_post_meta($post_id, 'isbn', true)) . '" placeholder="9781234567890">';
echo '<span class="bd-hint">' . esc_html__('13 digits, no dashes (e.g., 9781234567890)', 'bd-bookshelfhelper') . '</span>'; echo '<span class="bd-hint">' . esc_html__('13 digits, no dashes (e.g., 9781234567890)', 'umbra-bookshelf') . '</span>';
echo '</div>'; echo '</div>';
echo '<div class="bd-field">'; echo '<div class="bd-field">';
echo '<label for="asin">' . esc_html__('ASIN (Amazon)', 'bd-bookshelfhelper') . '</label>'; echo '<label for="asin">' . esc_html__('ASIN (Amazon)', 'umbra-bookshelf') . '</label>';
echo '<input type="text" id="asin" name="asin" value="' . esc_attr(get_post_meta($post_id, 'asin', true)) . '" placeholder="B001234567">'; echo '<input type="text" id="asin" name="asin" value="' . esc_attr(get_post_meta($post_id, 'asin', true)) . '" placeholder="B001234567">';
echo '<span class="bd-hint">' . esc_html__('10 alphanumeric characters (e.g., B001234567)', 'bd-bookshelfhelper') . '</span>'; echo '<span class="bd-hint">' . esc_html__('10 alphanumeric characters (e.g., B001234567)', 'umbra-bookshelf') . '</span>';
echo '</div>'; echo '</div>';
echo '</div>'; echo '</div>';
echo '</div>'; // End Identifiers echo '</div>'; // End Identifiers
// Section 3: Series & Order // Section 3: Series & Order
$this->render_section_header('series', __('Series & Order', 'bd-bookshelfhelper'), true); $this->render_section_header('series', __('Series & Order', 'umbra-bookshelf'), true);
echo '<div class="bd-section-content" id="bd-section-series">'; echo '<div class="bd-section-content" id="bd-section-series">';
$series_terms = get_terms(array('taxonomy' => 'series', 'hide_empty' => false)); $series_terms = get_terms(array('taxonomy' => 'series', 'hide_empty' => false));
echo '<div class="bd-field">'; echo '<div class="bd-field">';
echo '<label for="series_id">' . esc_html__('Series', 'bd-bookshelfhelper') . '</label>'; echo '<label for="series_id">' . esc_html__('Series', 'umbra-bookshelf') . '</label>';
echo '<select id="series_id" name="series_id">'; echo '<select id="series_id" name="series_id">';
echo '<option value="">' . esc_html__('None / Standalone', 'bd-bookshelfhelper') . '</option>'; echo '<option value="">' . esc_html__('None / Standalone', 'umbra-bookshelf') . '</option>';
$current_series = get_the_terms($post_id, 'series'); $current_series = get_the_terms($post_id, 'series');
$current_series_id = ($current_series && !is_wp_error($current_series)) ? $current_series[0]->term_id : 0; $current_series_id = ($current_series && !is_wp_error($current_series)) ? $current_series[0]->term_id : 0;
@@ -472,36 +484,36 @@ class BD_BookShelfHelper_Admin {
} }
} }
echo '</select>'; echo '</select>';
echo '<span class="bd-hint">' . esc_html__('Select the series this book belongs to, or leave blank for standalone.', 'bd-bookshelfhelper') . '</span>'; echo '<span class="bd-hint">' . esc_html__('Select the series this book belongs to, or leave blank for standalone.', 'umbra-bookshelf') . '</span>';
echo '</div>'; echo '</div>';
echo '<div class="bd-field">'; echo '<div class="bd-field">';
echo '<label for="volume_number">' . esc_html__('Volume Number', 'bd-bookshelfhelper') . '</label>'; echo '<label for="volume_number">' . esc_html__('Volume Number', 'umbra-bookshelf') . '</label>';
echo '<input type="number" id="volume_number" name="volume_number" value="' . esc_attr(get_post_meta($post_id, 'volume_number', true)) . '" min="1">'; echo '<input type="number" id="volume_number" name="volume_number" value="' . esc_attr(get_post_meta($post_id, 'volume_number', true)) . '" min="1">';
echo '<span class="bd-hint">' . esc_html__('Order within the series (e.g., 1 for Book 1).', 'bd-bookshelfhelper') . '</span>'; echo '<span class="bd-hint">' . esc_html__('Order within the series (e.g., 1 for Book 1).', 'umbra-bookshelf') . '</span>';
echo '</div>'; echo '</div>';
echo '</div>'; // End Series echo '</div>'; // End Series
// Section 4: Short Link // Section 4: Short Link
$this->render_section_header('shortlink', __('Short Link', 'bd-bookshelfhelper'), false); $this->render_section_header('shortlink', __('Short Link', 'umbra-bookshelf'), false);
echo '<div class="bd-section-content" id="bd-section-shortlink">'; echo '<div class="bd-section-content" id="bd-section-shortlink">';
echo '<div class="bd-field">'; echo '<div class="bd-field">';
echo '<label for="short_link_slug">' . esc_html__('Short Link Slug', 'bd-bookshelfhelper') . '</label>'; echo '<label for="short_link_slug">' . esc_html__('Short Link Slug', 'umbra-bookshelf') . '</label>';
$current_slug = get_post_meta($post_id, 'short_link_slug', true); $current_slug = get_post_meta($post_id, 'short_link_slug', true);
if (empty($current_slug)) { if (empty($current_slug)) {
$current_slug = $this->generate_short_slug($post->post_title); $current_slug = $this->generate_short_slug($post->post_title);
} }
echo '<code>' . esc_url(home_url('/')) . '</code>'; echo '<code>' . esc_url(home_url('/')) . '</code>';
echo '<input type="text" id="short_link_slug" name="short_link_slug" value="' . esc_attr($current_slug) . '">'; echo '<input type="text" id="short_link_slug" name="short_link_slug" value="' . esc_attr($current_slug) . '">';
echo '<span class="bd-hint">' . esc_html__('Auto-generated from title if empty. E.g., "ph" for "Pandemic Hacker"', 'bd-bookshelfhelper') . '</span>'; echo '<span class="bd-hint">' . esc_html__('Auto-generated from title if empty. E.g., "ph" for "Pandemic Hacker"', 'umbra-bookshelf') . '</span>';
echo '</div>'; echo '</div>';
echo '</div>'; // End Short Link echo '</div>'; // End Short Link
// Section 5: Language Content (tabs for each language) // Section 5: Language Content (tabs for each language)
$this->render_section_header('content', __('Content (by Language)', 'bd-bookshelfhelper'), true); $this->render_section_header('content', __('Content (by Language)', 'umbra-bookshelf'), true);
echo '<div class="bd-section-content" id="bd-section-content">'; echo '<div class="bd-section-content" id="bd-section-content">';
foreach ($languages as $code => $name) { foreach ($languages as $code => $name) {
@@ -509,48 +521,48 @@ class BD_BookShelfHelper_Admin {
echo '<h3>' . esc_html($name) . ' (' . strtoupper($code) . ')</h3>'; echo '<h3>' . esc_html($name) . ' (' . strtoupper($code) . ')</h3>';
echo '<div class="bd-field">'; echo '<div class="bd-field">';
echo '<label for="title_' . esc_attr($code) . '">' . esc_html__('Title', 'bd-bookshelfhelper') . '</label>'; echo '<label for="title_' . esc_attr($code) . '">' . esc_html__('Title', 'umbra-bookshelf') . '</label>';
echo '<input type="text" id="title_' . esc_attr($code) . '" name="title_' . esc_attr($code) . '" value="' . esc_attr(get_post_meta($post_id, 'title_' . $code, true)) . '" class="large-text">'; echo '<input type="text" id="title_' . esc_attr($code) . '" name="title_' . esc_attr($code) . '" value="' . esc_attr(get_post_meta($post_id, 'title_' . $code, true)) . '" class="large-text">';
echo '<span class="bd-hint">' . esc_html__('Title in this language (if different from default).', 'bd-bookshelfhelper') . '</span>'; echo '<span class="bd-hint">' . esc_html__('Title in this language (if different from default).', 'umbra-bookshelf') . '</span>';
echo '</div>'; echo '</div>';
echo '<div class="bd-field">'; echo '<div class="bd-field">';
echo '<label for="blurb_' . esc_attr($code) . '">' . esc_html__('Back Cover Blurb', 'bd-bookshelfhelper') . '</label>'; echo '<label for="blurb_' . esc_attr($code) . '">' . esc_html__('Back Cover Blurb', 'umbra-bookshelf') . '</label>';
echo '<textarea id="blurb_' . esc_attr($code) . '" name="blurb_' . esc_attr($code) . '" rows="6" class="large-text">' . esc_textarea(get_post_meta($post_id, 'blurb_' . $code, true)) . '</textarea>'; echo '<textarea id="blurb_' . esc_attr($code) . '" name="blurb_' . esc_attr($code) . '" rows="6" class="large-text">' . esc_textarea(get_post_meta($post_id, 'blurb_' . $code, true)) . '</textarea>';
echo '<span class="bd-hint">' . esc_html__('The book description shown on the back cover and product pages.', 'bd-bookshelfhelper') . '</span>'; echo '<span class="bd-hint">' . esc_html__('The book description shown on the back cover and product pages.', 'umbra-bookshelf') . '</span>';
echo '</div>'; echo '</div>';
echo '<div class="bd-field">'; echo '<div class="bd-field">';
echo '<label for="preview_excerpt_' . esc_attr($code) . '">' . esc_html__('Preview Excerpt', 'bd-bookshelfhelper') . '</label>'; echo '<label for="preview_excerpt_' . esc_attr($code) . '">' . esc_html__('Preview Excerpt', 'umbra-bookshelf') . '</label>';
echo '<textarea id="preview_excerpt_' . esc_attr($code) . '" name="preview_excerpt_' . esc_attr($code) . '" rows="4" class="large-text">' . esc_textarea(get_post_meta($post_id, 'preview_excerpt_' . $code, true)) . '</textarea>'; echo '<textarea id="preview_excerpt_' . esc_attr($code) . '" name="preview_excerpt_' . esc_attr($code) . '" rows="4" class="large-text">' . esc_textarea(get_post_meta($post_id, 'preview_excerpt_' . $code, true)) . '</textarea>';
echo '<span class="bd-hint">' . esc_html__('A short excerpt (50-100 words) for previews and "Look Inside" feature.', 'bd-bookshelfhelper') . '</span>'; echo '<span class="bd-hint">' . esc_html__('A short excerpt (50-100 words) for previews and "Look Inside" feature.', 'umbra-bookshelf') . '</span>';
echo '</div>'; echo '</div>';
echo '<div class="bd-field">'; echo '<div class="bd-field">';
echo '<label>' . esc_html__('Pull Quotes (up to 5)', 'bd-bookshelfhelper') . '</label>'; echo '<label>' . esc_html__('Pull Quotes (up to 5)', 'umbra-bookshelf') . '</label>';
echo '<div class="bd-pull-quotes">'; echo '<div class="bd-pull-quotes">';
for ($i = 1; $i <= 5; $i++) { for ($i = 1; $i <= 5; $i++) {
printf('<input type="text" name="pull_quote_%s_%d" value="%s" placeholder="%s">', printf('<input type="text" name="pull_quote_%s_%d" value="%s" placeholder="%s">',
esc_attr($code), $i, esc_attr($code), $i,
esc_attr(get_post_meta($post_id, 'pull_quote_' . $code . '_' . $i, true)), esc_attr(get_post_meta($post_id, 'pull_quote_' . $code . '_' . $i, true)),
esc_attr(sprintf(__('Quote %d', 'bd-bookshelfhelper'), $i)) esc_attr(sprintf(__('Quote %d', 'umbra-bookshelf'), $i))
); );
} }
echo '</div>'; echo '</div>';
echo '<span class="bd-hint">' . esc_html__('Notable quotes or reviews to display on the book page.', 'bd-bookshelfhelper') . '</span>'; echo '<span class="bd-hint">' . esc_html__('Notable quotes or reviews to display on the book page.', 'umbra-bookshelf') . '</span>';
echo '</div>'; echo '</div>';
echo '<div class="bd-field-row">'; echo '<div class="bd-field-row">';
echo '<div class="bd-field">'; echo '<div class="bd-field">';
echo '<label for="genre_tags_' . esc_attr($code) . '">' . esc_html__('Genre Tags', 'bd-bookshelfhelper') . '</label>'; echo '<label for="genre_tags_' . esc_attr($code) . '">' . esc_html__('Genre Tags', 'umbra-bookshelf') . '</label>';
echo '<input type="text" id="genre_tags_' . esc_attr($code) . '" name="genre_tags_' . esc_attr($code) . '" value="' . esc_attr(get_post_meta($post_id, 'genre_tags_' . $code, true)) . '">'; echo '<input type="text" id="genre_tags_' . esc_attr($code) . '" name="genre_tags_' . esc_attr($code) . '" value="' . esc_attr(get_post_meta($post_id, 'genre_tags_' . $code, true)) . '">';
echo '<span class="bd-hint">' . esc_html__('Comma-separated (e.g., Thriller, Sci-Fi, Mystery) - minimum 3 recommended', 'bd-bookshelfhelper') . '</span>'; echo '<span class="bd-hint">' . esc_html__('Comma-separated (e.g., Thriller, Sci-Fi, Mystery) - minimum 3 recommended', 'umbra-bookshelf') . '</span>';
echo '</div>'; echo '</div>';
echo '<div class="bd-field">'; echo '<div class="bd-field">';
echo '<label for="amazon_search_terms_' . esc_attr($code) . '">' . esc_html__('Amazon Search Terms', 'bd-bookshelfhelper') . '</label>'; echo '<label for="amazon_search_terms_' . esc_attr($code) . '">' . esc_html__('Amazon Search Terms', 'umbra-bookshelf') . '</label>';
echo '<input type="text" id="amazon_search_terms_' . esc_attr($code) . '" name="amazon_search_terms_' . esc_attr($code) . '" value="' . esc_attr(get_post_meta($post_id, 'amazon_search_terms_' . $code, true)) . '">'; echo '<input type="text" id="amazon_search_terms_' . esc_attr($code) . '" name="amazon_search_terms_' . esc_attr($code) . '" value="' . esc_attr(get_post_meta($post_id, 'amazon_search_terms_' . $code, true)) . '">';
echo '<span class="bd-hint">' . esc_html__('Keywords for Amazon search optimization, comma-separated.', 'bd-bookshelfhelper') . '</span>'; echo '<span class="bd-hint">' . esc_html__('Keywords for Amazon search optimization, comma-separated.', 'umbra-bookshelf') . '</span>';
echo '</div>'; echo '</div>';
echo '</div>'; echo '</div>';
@@ -560,7 +572,7 @@ class BD_BookShelfHelper_Admin {
echo '</div>'; // End Content echo '</div>'; // End Content
// Section 6: Retailer Links // Section 6: Retailer Links
$this->render_section_header('retailers', __('Retailer Links', 'bd-bookshelfhelper'), true); $this->render_section_header('retailers', __('Retailer Links', 'umbra-bookshelf'), true);
echo '<div class="bd-section-content" id="bd-section-retailers">'; echo '<div class="bd-section-content" id="bd-section-retailers">';
echo '<div class="bd-retailer-grid">'; echo '<div class="bd-retailer-grid">';
@@ -578,9 +590,9 @@ class BD_BookShelfHelper_Admin {
foreach ($retailers as $field => $info) { foreach ($retailers as $field => $info) {
echo '<div class="bd-field">'; echo '<div class="bd-field">';
echo '<label for="' . esc_attr($field) . '">' . esc_html($info['label'], 'bd-bookshelfhelper') . '</label>'; echo '<label for="' . esc_attr($field) . '">' . esc_html($info['label'], 'umbra-bookshelf') . '</label>';
echo '<input type="url" id="' . esc_attr($field) . '" name="' . esc_attr($field) . '" value="' . esc_url(get_post_meta($post_id, $field, true)) . '" placeholder="https://">'; echo '<input type="url" id="' . esc_attr($field) . '" name="' . esc_attr($field) . '" value="' . esc_url(get_post_meta($post_id, $field, true)) . '" placeholder="https://">';
echo '<span class="bd-hint">' . esc_html($info['hint'], 'bd-bookshelfhelper') . '</span>'; echo '<span class="bd-hint">' . esc_html($info['hint'], 'umbra-bookshelf') . '</span>';
echo '</div>'; echo '</div>';
} }
@@ -588,13 +600,13 @@ class BD_BookShelfHelper_Admin {
echo '</div>'; // End Retailer Links echo '</div>'; // End Retailer Links
// Section 7: Reviews // Section 7: Reviews
$this->render_section_header('reviews', __('Reviews', 'bd-bookshelfhelper'), false); $this->render_section_header('reviews', __('Reviews', 'umbra-bookshelf'), false);
echo '<div class="bd-section-content" id="bd-section-reviews">'; echo '<div class="bd-section-content" id="bd-section-reviews">';
$reviews_table = $wpdb->prefix . 'bd_reviews'; $reviews_table = $wpdb->prefix . 'bd_reviews';
$reviews = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$reviews_table} WHERE book_id = %d ORDER BY display_order", $post_id)); $reviews = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$reviews_table} WHERE book_id = %d ORDER BY display_order", $post_id));
echo '<p>' . esc_html__('Reviews are managed via the Reviews tab on the book editing screen.', 'bd-bookshelfhelper') . '</p>'; echo '<p>' . esc_html__('Reviews are managed via the Reviews tab on the book editing screen.', 'umbra-bookshelf') . '</p>';
if ($reviews) { if ($reviews) {
echo '<table class="widefat">'; echo '<table class="widefat">';
@@ -613,48 +625,48 @@ class BD_BookShelfHelper_Admin {
echo '</div>'; // End Reviews echo '</div>'; // End Reviews
// Section 8: Ratings // Section 8: Ratings
$this->render_section_header('ratings', __('Ratings', 'bd-bookshelfhelper'), false); $this->render_section_header('ratings', __('Ratings', 'umbra-bookshelf'), false);
echo '<div class="bd-section-content" id="bd-section-ratings">'; echo '<div class="bd-section-content" id="bd-section-ratings">';
echo '<div class="bd-field-row">'; echo '<div class="bd-field-row">';
echo '<div class="bd-field">'; echo '<div class="bd-field">';
echo '<label for="amazon_rating">' . esc_html__('Amazon Rating', 'bd-bookshelfhelper') . '</label>'; echo '<label for="amazon_rating">' . esc_html__('Amazon Rating', 'umbra-bookshelf') . '</label>';
echo '<input type="text" id="amazon_rating" name="amazon_rating" value="' . esc_attr(get_post_meta($post_id, 'amazon_rating', true)) . '" placeholder="4.5">'; echo '<input type="text" id="amazon_rating" name="amazon_rating" value="' . esc_attr(get_post_meta($post_id, 'amazon_rating', true)) . '" placeholder="4.5">';
echo '<span class="bd-hint">' . esc_html__('Average rating out of 5 (e.g., 4.5)', 'bd-bookshelfhelper') . '</span>'; echo '<span class="bd-hint">' . esc_html__('Average rating out of 5 (e.g., 4.5)', 'umbra-bookshelf') . '</span>';
echo '</div>'; echo '</div>';
echo '<div class="bd-field">'; echo '<div class="bd-field">';
echo '<label for="goodreads_rating">' . esc_html__('Goodreads Rating', 'bd-bookshelfhelper') . '</label>'; echo '<label for="goodreads_rating">' . esc_html__('Goodreads Rating', 'umbra-bookshelf') . '</label>';
echo '<input type="text" id="goodreads_rating" name="goodreads_rating" value="' . esc_attr(get_post_meta($post_id, 'goodreads_rating', true)) . '" placeholder="4.2">'; echo '<input type="text" id="goodreads_rating" name="goodreads_rating" value="' . esc_attr(get_post_meta($post_id, 'goodreads_rating', true)) . '" placeholder="4.2">';
echo '<span class="bd-hint">' . esc_html__('Average rating out of 5 from Goodreads.', 'bd-bookshelfhelper') . '</span>'; echo '<span class="bd-hint">' . esc_html__('Average rating out of 5 from Goodreads.', 'umbra-bookshelf') . '</span>';
echo '</div>'; echo '</div>';
echo '</div>'; echo '</div>';
echo '</div>'; // End Ratings echo '</div>'; // End Ratings
// Section 9: SEO // Section 9: SEO
$this->render_section_header('seo', __('SEO Settings', 'bd-bookshelfhelper'), false); $this->render_section_header('seo', __('SEO Settings', 'umbra-bookshelf'), false);
echo '<div class="bd-section-content" id="bd-section-seo">'; echo '<div class="bd-section-content" id="bd-section-seo">';
echo '<div class="bd-field">'; echo '<div class="bd-field">';
echo '<label for="seo_title">' . esc_html__('SEO Title', 'bd-bookshelfhelper') . '</label>'; echo '<label for="seo_title">' . esc_html__('SEO Title', 'umbra-bookshelf') . '</label>';
echo '<input type="text" id="seo_title" name="seo_title" value="' . esc_attr(get_post_meta($post_id, 'seo_title', true)) . '" class="large-text">'; echo '<input type="text" id="seo_title" name="seo_title" value="' . esc_attr(get_post_meta($post_id, 'seo_title', true)) . '" class="large-text">';
echo '<span class="bd-hint">' . esc_html__('Custom title for search engines (defaults to book title if empty).', 'bd-bookshelfhelper') . '</span>'; echo '<span class="bd-hint">' . esc_html__('Custom title for search engines (defaults to book title if empty).', 'umbra-bookshelf') . '</span>';
echo '</div>'; echo '</div>';
echo '<div class="bd-field">'; echo '<div class="bd-field">';
echo '<label for="seo_description">' . esc_html__('SEO Description', 'bd-bookshelfhelper') . '</label>'; echo '<label for="seo_description">' . esc_html__('SEO Description', 'umbra-bookshelf') . '</label>';
echo '<textarea id="seo_description" name="seo_description" rows="4" class="large-text">' . esc_textarea(get_post_meta($post_id, 'seo_description', true)) . '</textarea>'; echo '<textarea id="seo_description" name="seo_description" rows="4" class="large-text">' . esc_textarea(get_post_meta($post_id, 'seo_description', true)) . '</textarea>';
echo '<span class="bd-hint">' . esc_html__('Meta description for search results (150-160 characters recommended).', 'bd-bookshelfhelper') . '</span>'; echo '<span class="bd-hint">' . esc_html__('Meta description for search results (150-160 characters recommended).', 'umbra-bookshelf') . '</span>';
echo '</div>'; echo '</div>';
echo '</div>'; // End SEO echo '</div>'; // End SEO
// Action buttons // Action buttons
echo '<div class="bd-data-entry-actions">'; echo '<div class="bd-data-entry-actions">';
echo '<a href="' . esc_url(admin_url('post.php?post=' . $post_id . '&action=edit')) . '" class="button">' . esc_html__('Edit in Standard Editor', 'bd-bookshelfhelper') . '</a>'; echo '<a href="' . esc_url(admin_url('post.php?post=' . $post_id . '&action=edit')) . '" class="button">' . esc_html__('Edit in Standard Editor', 'umbra-bookshelf') . '</a>';
submit_button(esc_html__('Save Book Data', 'bd-bookshelfhelper'), 'primary large', 'bd_save_data_entry', false); submit_button(esc_html__('Save Book Data', 'umbra-bookshelf'), 'primary large', 'bd_save_data_entry', false);
echo '<button type="button" id="bd-preview-btn" class="button button-large">' . esc_html__('Preview', 'bd-bookshelfhelper') . '</button>'; echo '<button type="button" id="bd-preview-btn" class="button button-large">' . esc_html__('Preview', 'umbra-bookshelf') . '</button>';
echo '</div>'; echo '</div>';
echo '</form>'; echo '</form>';
@@ -685,12 +697,12 @@ class BD_BookShelfHelper_Admin {
*/ */
private function save_data_entry($post_id) { private function save_data_entry($post_id) {
if (!isset($_POST['bd_data_entry_save_nonce']) || !wp_verify_nonce($_POST['bd_data_entry_save_nonce'], 'bd_data_entry_save')) { if (!isset($_POST['bd_data_entry_save_nonce']) || !wp_verify_nonce($_POST['bd_data_entry_save_nonce'], 'bd_data_entry_save')) {
echo '<div class="bd-status-message error">' . esc_html__('Security check failed.', 'bd-bookshelfhelper') . '</div>'; echo '<div class="bd-status-message error">' . esc_html__('Security check failed.', 'umbra-bookshelf') . '</div>';
return; return;
} }
if (!current_user_can('edit_post', $post_id)) { if (!current_user_can('edit_post', $post_id)) {
echo '<div class="bd-status-message error">' . esc_html__('Permission denied.', 'bd-bookshelfhelper') . '</div>'; echo '<div class="bd-status-message error">' . esc_html__('Permission denied.', 'umbra-bookshelf') . '</div>';
return; return;
} }
@@ -724,7 +736,7 @@ class BD_BookShelfHelper_Admin {
} }
// Language content // Language content
$languages = BD_BookShelfHelper::get_supported_languages(); $languages = Umbra_Bookshelf_Helper::get_supported_languages();
foreach ($languages as $code => $name) { foreach ($languages as $code => $name) {
$title_key = 'title_' . $code; $title_key = 'title_' . $code;
$blurb_key = 'blurb_' . $code; $blurb_key = 'blurb_' . $code;
@@ -769,7 +781,7 @@ class BD_BookShelfHelper_Admin {
} }
} }
echo '<div class="bd-status-message success">' . esc_html__('Book data saved successfully!', 'bd-bookshelfhelper') . '</div>'; echo '<div class="bd-status-message success">' . esc_html__('Book data saved successfully!', 'umbra-bookshelf') . '</div>';
} }
/** /**
@@ -815,7 +827,7 @@ class BD_BookShelfHelper_Admin {
} }
// Save language content // Save language content
$languages = BD_BookShelfHelper::get_supported_languages(); $languages = Umbra_Bookshelf_Helper::get_supported_languages();
foreach ($languages as $code => $name) { foreach ($languages as $code => $name) {
$title_key = 'title_' . $code; $title_key = 'title_' . $code;
$blurb_key = 'blurb_' . $code; $blurb_key = 'blurb_' . $code;
@@ -882,7 +894,7 @@ class BD_BookShelfHelper_Admin {
} }
} }
$languages = BD_BookShelfHelper::get_supported_languages(); $languages = Umbra_Bookshelf_Helper::get_supported_languages();
foreach ($languages as $code => $name) { foreach ($languages as $code => $name) {
$title_key = 'title_' . $code; $title_key = 'title_' . $code;
@@ -996,7 +1008,7 @@ class BD_BookShelfHelper_Admin {
'active' => 1, 'active' => 1,
'created_at' => current_time('mysql'), 'created_at' => current_time('mysql'),
)); ));
echo '<div class="updated"><p>' . esc_html__('Pen name added.', 'bd-bookshelfhelper') . '</p></div>'; echo '<div class="updated"><p>' . esc_html__('Pen name added.', 'umbra-bookshelf') . '</p></div>';
} }
} }
@@ -1014,24 +1026,24 @@ class BD_BookShelfHelper_Admin {
$pen_names = $wpdb->get_results("SELECT * FROM {$table_name} ORDER BY pen_name"); $pen_names = $wpdb->get_results("SELECT * FROM {$table_name} ORDER BY pen_name");
echo '<div class="wrap">'; echo '<div class="wrap">';
echo '<h1>' . esc_html__('Pen Names', 'bd-bookshelfhelper') . '</h1>'; echo '<h1>' . esc_html__('Pen Names', 'umbra-bookshelf') . '</h1>';
// Add new form // Add new form
echo '<form method="post">'; echo '<form method="post">';
wp_nonce_field('bd_add_pen_name'); wp_nonce_field('bd_add_pen_name');
echo '<h2>' . esc_html__('Add New Pen Name', 'bd-bookshelfhelper') . '</h2>'; echo '<h2>' . esc_html__('Add New Pen Name', 'umbra-bookshelf') . '</h2>';
echo '<table class="form-table">'; echo '<table class="form-table">';
echo '<tr><th>' . esc_html__('Pen Name', 'bd-bookshelfhelper') . '</th>'; echo '<tr><th>' . esc_html__('Pen Name', 'umbra-bookshelf') . '</th>';
echo '<td><input type="text" name="new_pen_name" class="regular-text" required>'; echo '<td><input type="text" name="new_pen_name" class="regular-text" required>';
echo '<p class="description">' . esc_html__('e.g., J.K. Murphy', 'bd-bookshelfhelper') . '</p></td></tr>'; echo '<p class="description">' . esc_html__('e.g., J.K. Murphy', 'umbra-bookshelf') . '</p></td></tr>';
echo '<tr><th>' . esc_html__('Notes', 'bd-bookshelfhelper') . '</th>'; echo '<tr><th>' . esc_html__('Notes', 'umbra-bookshelf') . '</th>';
echo '<td><textarea name="new_notes" rows="3" class="regular-text"></textarea></td></tr>'; echo '<td><textarea name="new_notes" rows="3" class="regular-text"></textarea></td></tr>';
echo '</table>'; echo '</table>';
submit_button(esc_html__('Add Pen Name', 'bd-bookshelfhelper'), 'primary', 'bd_add_pen_name'); submit_button(esc_html__('Add Pen Name', 'umbra-bookshelf'), 'primary', 'bd_add_pen_name');
echo '</form>'; echo '</form>';
// Existing list // Existing list
echo '<h2>' . esc_html__('Existing Pen Names', 'bd-bookshelfhelper') . '</h2>'; echo '<h2>' . esc_html__('Existing Pen Names', 'umbra-bookshelf') . '</h2>';
echo '<table class="widefat">'; echo '<table class="widefat">';
echo '<thead><tr><th>ID</th><th>Pen Name</th><th>Notes</th><th>Status</th><th>Actions</th></tr></thead>'; echo '<thead><tr><th>ID</th><th>Pen Name</th><th>Notes</th><th>Status</th><th>Actions</th></tr></thead>';
echo '<tbody>'; echo '<tbody>';
@@ -1042,21 +1054,21 @@ class BD_BookShelfHelper_Admin {
printf('<td>%d</td>', esc_html($pn->id)); printf('<td>%d</td>', esc_html($pn->id));
printf('<td>%s</td>', esc_html($pn->pen_name)); printf('<td>%s</td>', esc_html($pn->pen_name));
printf('<td>%s</td>', esc_html($pn->notes)); printf('<td>%s</td>', esc_html($pn->notes));
printf('<td>%s</td>', $pn->active ? esc_html__('Active', 'bd-bookshelfhelper') : esc_html__('Inactive', 'bd-bookshelfhelper')); printf('<td>%s</td>', $pn->active ? esc_html__('Active', 'umbra-bookshelf') : esc_html__('Inactive', 'umbra-bookshelf'));
echo '<td>'; echo '<td>';
printf('<form method="post" style="display:inline;">%s', wp_nonce_field('bd_update_pen_status', '_wpnonce', true, false)); printf('<form method="post" style="display:inline;">%s', wp_nonce_field('bd_update_pen_status', '_wpnonce', true, false));
printf('<input type="hidden" name="pen_name_id" value="%d">', esc_attr($pn->id)); printf('<input type="hidden" name="pen_name_id" value="%d">', esc_attr($pn->id));
printf('<input type="checkbox" name="pen_active" value="1"%s>', checked($pn->active, 1, false)); printf('<input type="checkbox" name="pen_active" value="1"%s>', checked($pn->active, 1, false));
submit_button(esc_html__('Update', 'bd-bookshelfhelper'), 'secondary', 'bd_update_pen_status', false); submit_button(esc_html__('Update', 'umbra-bookshelf'), 'secondary', 'bd_update_pen_status', false);
echo '</form> '; echo '</form> ';
printf('<form method="post" style="display:inline;">%s', wp_nonce_field('bd_delete_pen_name', '_wpnonce', true, false)); printf('<form method="post" style="display:inline;">%s', wp_nonce_field('bd_delete_pen_name', '_wpnonce', true, false));
printf('<input type="hidden" name="pen_name_id" value="%d">', esc_attr($pn->id)); printf('<input type="hidden" name="pen_name_id" value="%d">', esc_attr($pn->id));
submit_button(esc_html__('Delete', 'bd-bookshelfhelper'), 'delete', 'bd_delete_pen_name', false, array('onclick' => 'return confirm("Delete this pen name?");')); submit_button(esc_html__('Delete', 'umbra-bookshelf'), 'delete', 'bd_delete_pen_name', false, array('onclick' => 'return confirm("Delete this pen name?");'));
echo '</form>'; echo '</form>';
echo '</td></tr>'; echo '</td></tr>';
} }
} else { } else {
echo '<tr><td colspan="5">' . esc_html__('No pen names found.', 'bd-bookshelfhelper') . '</td></tr>'; echo '<tr><td colspan="5">' . esc_html__('No pen names found.', 'umbra-bookshelf') . '</td></tr>';
} }
echo '</tbody></table></div>'; echo '</tbody></table></div>';
@@ -1081,20 +1093,20 @@ class BD_BookShelfHelper_Admin {
$goodreads_id = sanitize_text_field($_POST['goodreads_author_id'] ?? ''); $goodreads_id = sanitize_text_field($_POST['goodreads_author_id'] ?? '');
update_option('bdm_goodreads_author_id', $goodreads_id); update_option('bdm_goodreads_author_id', $goodreads_id);
echo '<div class="updated"><p>' . esc_html__('Settings saved.', 'bd-bookshelfhelper') . '</p></div>'; echo '<div class="updated"><p>' . esc_html__('Settings saved.', 'umbra-bookshelf') . '</p></div>';
} }
$affiliates = get_option('bdm_affiliate_codes', array()); $affiliates = get_option('bdm_affiliate_codes', array());
$goodreads_id = get_option('bdm_goodreads_author_id', '30469651'); $goodreads_id = get_option('bdm_goodreads_author_id', '30469651');
echo '<div class="wrap">'; echo '<div class="wrap">';
echo '<h1>' . esc_html__('BD BookHelper Settings', 'bd-bookshelfhelper') . '</h1>'; echo '<h1>' . esc_html__('BD BookHelper Settings', 'umbra-bookshelf') . '</h1>';
echo '<form method="post">'; echo '<form method="post">';
wp_nonce_field('bd_settings'); wp_nonce_field('bd_settings');
echo '<h2>' . esc_html__('Affiliate Codes', 'bd-bookshelfhelper') . '</h2>'; echo '<h2>' . esc_html__('Affiliate Codes', 'umbra-bookshelf') . '</h2>';
echo '<p>' . esc_html__('Enter your affiliate IDs for each retailer. Links include these IDs when available.', 'bd-bookshelfhelper') . '</p>'; echo '<p>' . esc_html__('Enter your affiliate IDs for each retailer. Links include these IDs when available.', 'umbra-bookshelf') . '</p>';
echo '<table class="form-table">'; echo '<table class="form-table">';
$affiliate_fields = array( $affiliate_fields = array(
@@ -1113,17 +1125,17 @@ class BD_BookShelfHelper_Admin {
} }
echo '</table>'; echo '</table>';
echo '<h2>' . esc_html__('Goodreads Integration', 'bd-bookshelfhelper') . '</h2>'; echo '<h2>' . esc_html__('Goodreads Integration', 'umbra-bookshelf') . '</h2>';
echo '<table class="form-table">'; echo '<table class="form-table">';
printf('<tr><th>' . esc_html__('Goodreads Author ID', 'bd-bookshelfhelper') . '</th>'); printf('<tr><th>' . esc_html__('Goodreads Author ID', 'umbra-bookshelf') . '</th>');
printf('<td><input type="text" name="goodreads_author_id" value="%s" class="regular-text">', esc_attr($goodreads_id)); printf('<td><input type="text" name="goodreads_author_id" value="%s" class="regular-text">', esc_attr($goodreads_id));
echo '<p class="description">' . esc_html__('Default: 30469651 (B_D_Murphy)', 'bd-bookshelfhelper') . '</p></td></tr>'; echo '<p class="description">' . esc_html__('Default: 30469651 (B_D_Murphy)', 'umbra-bookshelf') . '</p></td></tr>';
echo '</table>'; echo '</table>';
echo '<h2>' . esc_html__('Amazon Ratings Warning', 'bd-bookshelfhelper') . '</h2>'; echo '<h2>' . esc_html__('Amazon Ratings Warning', 'umbra-bookshelf') . '</h2>';
echo '<p style="color:#d63638;">' . esc_html__('Warning: Scraping Amazon product pages may conflict with their Terms of Service. Use at your own risk.', 'bd-bookshelfhelper') . '</p>'; echo '<p style="color:#d63638;">' . esc_html__('Warning: Scraping Amazon product pages may conflict with their Terms of Service. Use at your own risk.', 'umbra-bookshelf') . '</p>';
submit_button(esc_html__('Save Settings', 'bd-bookshelfhelper'), 'primary', 'bd_save_settings'); submit_button(esc_html__('Save Settings', 'umbra-bookshelf'), 'primary', 'bd_save_settings');
echo '</form></div>'; echo '</form></div>';
} }
@@ -1137,7 +1149,7 @@ class BD_BookShelfHelper_Admin {
$table_exists = $wpdb->get_var("SHOW TABLES LIKE '$old_table'") === $old_table; $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$old_table'") === $old_table;
if (isset($_POST['bd_run_migration']) && check_admin_referer('bd_migrate_nonce')) { if (isset($_POST['bd_run_migration']) && check_admin_referer('bd_migrate_nonce')) {
$result = BD_BookShelfHelper::migrate_from_bd_books(); $result = Umbra_Bookshelf_Helper::migrate_from_bd_books();
if ($result['success']) { if ($result['success']) {
echo '<div class="updated"><p>' . esc_html($result['message']) . '</p></div>'; echo '<div class="updated"><p>' . esc_html($result['message']) . '</p></div>';
} else { } else {
@@ -1146,13 +1158,13 @@ class BD_BookShelfHelper_Admin {
} }
echo '<div class="wrap">'; echo '<div class="wrap">';
echo '<h1>' . esc_html__('Migrate from BD Books', 'bd-bookshelfhelper') . '</h1>'; echo '<h1>' . esc_html__('Migrate from BD Books', 'umbra-bookshelf') . '</h1>';
if (!$table_exists) { if (!$table_exists) {
echo '<div class="notice notice-warning">'; echo '<div class="notice notice-warning">';
echo '<p>' . esc_html__('The old BD Books table was not found. No migration needed - the plugin data does not exist.', 'bd-bookshelfhelper') . '</p>'; echo '<p>' . esc_html__('The old BD Books table was not found. No migration needed - the plugin data does not exist.', 'umbra-bookshelf') . '</p>';
echo '</div>'; echo '</div>';
echo '<p>' . esc_html__('This tool migrates books from the original "BD Books" (BD Book Listing) plugin to the new BookHelper system using custom post types.', 'bd-bookshelfhelper') . '</p>'; echo '<p>' . esc_html__('This tool migrates books from the original "BD Books" (BD Book Listing) plugin to the new BookHelper system using custom post types.', 'umbra-bookshelf') . '</p>';
echo '</div>'; echo '</div>';
return; return;
} }
@@ -1167,12 +1179,12 @@ class BD_BookShelfHelper_Admin {
)); ));
echo '<div class="notice notice-info">'; echo '<div class="notice notice-info">';
echo '<p>' . sprintf(esc_html__('Found %d books in the old BD Books table.', 'bd-bookshelfhelper'), $old_count) . '</p>'; echo '<p>' . sprintf(esc_html__('Found %d books in the old BD Books table.', 'umbra-bookshelf'), $old_count) . '</p>';
echo '<p>' . sprintf(esc_html__('Current BookHelper has %d books.', 'bd-bookshelfhelper'), count($new_books)) . '</p>'; echo '<p>' . sprintf(esc_html__('Current BookHelper has %d books.', 'umbra-bookshelf'), count($new_books)) . '</p>';
echo '</div>'; echo '</div>';
if ($old_count > 0) { if ($old_count > 0) {
echo '<h2>' . esc_html__('Books to Migrate', 'bd-bookshelfhelper') . '</h2>'; echo '<h2>' . esc_html__('Books to Migrate', 'umbra-bookshelf') . '</h2>';
echo '<table class="widefat">'; echo '<table class="widefat">';
echo '<thead><tr><th>Title</th><th>Author</th><th>Cover</th></tr></thead>'; echo '<thead><tr><th>Title</th><th>Author</th><th>Cover</th></tr></thead>';
echo '<tbody>'; echo '<tbody>';
@@ -1181,25 +1193,25 @@ class BD_BookShelfHelper_Admin {
echo '<tr>'; echo '<tr>';
echo '<td>' . esc_html($book->title) . '</td>'; echo '<td>' . esc_html($book->title) . '</td>';
echo '<td>' . esc_html($book->author) . '</td>'; echo '<td>' . esc_html($book->author) . '</td>';
echo '<td>' . ($has_cover ? esc_html__('Yes', 'bd-bookshelfhelper') : esc_html__('No', 'bd-bookshelfhelper')) . '</td>'; echo '<td>' . ($has_cover ? esc_html__('Yes', 'umbra-bookshelf') : esc_html__('No', 'umbra-bookshelf')) . '</td>';
echo '</tr>'; echo '</tr>';
} }
echo '</tbody></table>'; echo '</tbody></table>';
echo '<p style="margin-top: 20px;"><strong>' . esc_html__('What gets migrated:', 'bd-bookshelfhelper') . '</strong></p>'; echo '<p style="margin-top: 20px;"><strong>' . esc_html__('What gets migrated:', 'umbra-bookshelf') . '</strong></p>';
echo '<ul>'; echo '<ul>';
echo '<li>' . esc_html__('Book titles and authors (as pen names)', 'bd-bookshelfhelper') . '</li>'; echo '<li>' . esc_html__('Book titles and authors (as pen names)', 'umbra-bookshelf') . '</li>';
echo '<li>' . esc_html__('Descriptions become blurbs in English', 'bd-bookshelfhelper') . '</li>'; echo '<li>' . esc_html__('Descriptions become blurbs in English', 'umbra-bookshelf') . '</li>';
echo '<li>' . esc_html__('Cover images (WordPress media or external URLs)', 'bd-bookshelfhelper') . '</li>'; echo '<li>' . esc_html__('Cover images (WordPress media or external URLs)', 'umbra-bookshelf') . '</li>';
echo '<li>' . esc_html__('Purchase links to retailer fields', 'bd-bookshelfhelper') . '</li>'; echo '<li>' . esc_html__('Purchase links to retailer fields', 'umbra-bookshelf') . '</li>';
echo '</ul>'; echo '</ul>';
echo '<form method="post" style="margin-top: 20px;">'; echo '<form method="post" style="margin-top: 20px;">';
wp_nonce_field('bd_migrate_nonce'); wp_nonce_field('bd_migrate_nonce');
echo '<input type="submit" name="bd_run_migration" class="button button-primary" value="' . esc_attr__('Run Migration', 'bd-bookshelfhelper') . '" onclick="return confirm(\'' . esc_js(__('This will create new book entries for any books not already migrated. Continue?', 'bd-bookshelfhelper')) . '\');">'; echo '<input type="submit" name="bd_run_migration" class="button button-primary" value="' . esc_attr__('Run Migration', 'umbra-bookshelf') . '" onclick="return confirm(\'' . esc_js(__('This will create new book entries for any books not already migrated. Continue?', 'umbra-bookshelf')) . '\');">';
echo '</form>'; echo '</form>';
} else { } else {
echo '<p>' . esc_html__('No books found in the old table to migrate.', 'bd-bookshelfhelper') . '</p>'; echo '<p>' . esc_html__('No books found in the old table to migrate.', 'umbra-bookshelf') . '</p>';
} }
echo '</div>'; echo '</div>';
@@ -1213,6 +1225,6 @@ class BD_BookShelfHelper_Admin {
$book_id = absint($_POST['book_id']); $book_id = absint($_POST['book_id']);
wp_send_json_success(array('message' => esc_html__('Links updated.', 'bd-bookshelfhelper'))); wp_send_json_success(array('message' => esc_html__('Links updated.', 'umbra-bookshelf')));
} }
} }

View File

@@ -5,14 +5,14 @@
/** /**
* Installation and Database Setup * Installation and Database Setup
* *
* @package BD_BookShelfHelper * @package Umbra_Bookshelf_Helper
*/ */
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
class BD_BookShelfHelper_Install { class Umbra_Bookshelf_Helper_Install {
/** /**
* Activate plugin * Activate plugin
*/ */
@@ -153,7 +153,7 @@ class BD_BookShelfHelper_Install {
* Create roles * Create roles
*/ */
private static function create_roles() { private static function create_roles() {
add_role('book_manager', esc_html__('Book Manager', 'bd-bookshelfhelper'), array( add_role('book_manager', esc_html__('Book Manager', 'umbra-bookshelf'), array(
'read' => true, 'read' => true,
'edit_books' => true, 'edit_books' => true,
'edit_others_books' => true, 'edit_others_books' => true,

View File

@@ -1,7 +1,7 @@
/** /**
* BD Bookshelf - Public Styles * BD Bookshelf - Public Styles
* *
* @package BD_Bookshelf * @package Umbra_Bookshelf
*/ */
/* Sell Grid */ /* Sell Grid */

View File

@@ -4,7 +4,7 @@
/** /**
* BD Bookshelf - GDPR Consent * BD Bookshelf - GDPR Consent
* *
* @package BD_Bookshelf * @package Umbra_Bookshelf
*/ */
(function($) { (function($) {
@@ -12,7 +12,7 @@
$(document).ready(function() { $(document).ready(function() {
// Check if consent already given // Check if consent already given
if (bdBookshelfHasConsent()) { if (umbraBookshelfHasConsent()) {
return; return;
} }
@@ -22,16 +22,16 @@
// Handle accept button // Handle accept button
$('#bd-bookshelf-gdpr-accept').on('click', function() { $('#bd-bookshelf-gdpr-accept').on('click', function() {
$.ajax({ $.ajax({
url: bdBookshelfGdpr.ajaxurl, url: umbraBookshelfGdpr.ajaxurl,
type: 'POST', type: 'POST',
data: { data: {
action: 'bd_bookshelf_accept_consent', action: 'bd_bookshelf_accept_consent',
nonce: bdBookshelfGdpr.nonce nonce: umbraBookshelfGdpr.nonce
}, },
success: function(response) { success: function(response) {
if (response.success) { if (response.success) {
// Set cookie // Set cookie
bdBookshelfSetCookie('bd_bookshelf_consent', '1', bdBookshelfGdpr.cookieDays); umbraBookshelfSetCookie('bd_bookshelf_consent', '1', umbraBookshelfGdpr.cookieDays);
// Hide banner // Hide banner
$('#bd-bookshelf-gdpr-banner').fadeOut(300); $('#bd-bookshelf-gdpr-banner').fadeOut(300);
@@ -41,18 +41,18 @@
}); });
}); });
function bdBookshelfHasConsent() { function umbraBookshelfHasConsent() {
return bdBookshelfGetCookie('bd_bookshelf_consent') === '1'; return umbraBookshelfGetCookie('bd_bookshelf_consent') === '1';
} }
function bdBookshelfSetCookie(name, value, days) { function umbraBookshelfSetCookie(name, value, days) {
var date = new Date(); var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
var expires = "expires=" + date.toUTCString(); var expires = "expires=" + date.toUTCString();
document.cookie = name + "=" + value + ";" + expires + ";path=/"; document.cookie = name + "=" + value + ";" + expires + ";path=/";
} }
function bdBookshelfGetCookie(name) { function umbraBookshelfGetCookie(name) {
var nameEQ = name + "="; var nameEQ = name + "=";
var ca = document.cookie.split(';'); var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) { for (var i = 0; i < ca.length; i++) {

View File

@@ -4,7 +4,7 @@
/** /**
* BD Bookshelf - Public JavaScript * BD Bookshelf - Public JavaScript
* *
* @package BD_Bookshelf * @package Umbra_Bookshelf
*/ */
(function($) { (function($) {

View File

@@ -0,0 +1,81 @@
=== Umbra Bookshelf ===
Contributors: umbratools
Tags: books, series, isbn, authors, newsletter, publishing
Requires at least: 6.0
Tested up to: 6.6
Requires PHP: 7.4
Stable tag: 1.0.0
License: Apache-2.0
License URI: https://www.apache.org/licenses/LICENSE-2.0
Complete book management for authors and publishers: catalogue and ISBN tracking, series, pen names, retail links, rating sync, GDPR consent, and newsletter delivery.
== Description ==
Umbra Bookshelf is a single plugin covering the full book workflow. It
consolidates three previously separate plugins:
* **Books** (formerly BD Bookshelf) — the catalogue: ISBN/ASIN variants,
author records, series, retail sites and per-format buy links, Goodreads and
Amazon rating synchronisation, SEO rows, shortlink click tracking, GDPR
consent banners, and CSV/post import.
* **Library** (formerly BD BookShelfHelper) — the WordPress-native model: the
`book` post type and `series` taxonomy, pen names, a focused data-entry
screen, shortlinks, reviews and ratings, plus a migration screen.
* **Email** (formerly BD SimpleEmail) — newsletter delivery: subscribers and
newsletters, a `[bdm_signup]` shortcode, signup and unsubscribe handling via
AJAX and `admin_post`, Gemini-assisted composition, and Gmail/SMTP sending.
Everything now sits under a single **Umbra Bookshelf** admin menu.
= Upgrading from the old plugins =
Umbra Bookshelf reads and writes the same database tables, option keys, post
meta, cron hooks and shortcodes as the three plugins it replaces. Existing data
is picked up as-is; no migration is required.
Install Umbra Bookshelf, then deactivate BD Bookshelf, BD BookShelfHelper and
BD SimpleEmail. **Do not delete** them from the Plugins page first — deleting
runs their uninstall routines, which drop your tables and options.
The shortcodes `[bd-book]`, `[bd-books]` and `[bdm_signup]` are unchanged, so
existing posts keep working. The admin menu locations have changed: see the
specification in `spec/SPEC_Umbra_Bookshelf_Plugin.md` for the full menu map.
== Frequently Asked Questions ==
= Do I need to migrate my books? =
No. Table names, option keys, post meta keys and shortcodes are deliberately
unchanged. Deactivate the old plugins rather than deleting them.
= The admin menu looks different. =
It is one menu now instead of three. The catalogue, library and email screens
are all still present as submenus, and Settings is split into catalogue
settings and library settings.
= Are there known problems? =
Yes — see `docs/KNOWN-ISSUES.md`. The most significant is that the catalogue and
library modules both create `bd_books` and `bd_series` with different schemas,
inherited from the old plugins. If you ran more than one of them together, check
that screen before relying on it.
== Changelog ==
= 1.0.0 =
* Initial release of the consolidated plugin.
* Merged BD Bookshelf, BD BookShelfHelper and BD SimpleEmail into one plugin
under a single admin menu.
* Renamed plugin headers, text domains, classes, constants, asset handles and
admin slugs to the Umbra Bookshelf identity.
* Preserved all database tables, option keys, post meta, cron hooks, nonces and
shortcodes, so existing installs need no migration.
* Registered the top-level admin menu once instead of three times.
* Fixed a duplicate-class fatal error between the catalogue's theme-compatibility
shim and the library module's helper class.
* Fixed duplicate AJAX handler, duplicate Settings submenu, and colliding asset
handles across modules.
* Added `spec/SPEC_Umbra_Bookshelf_Plugin.md`, `docs/KNOWN-ISSUES.md`,
`docs/ARCHITECTURE.md`, and the `tests/` verification suite.

View File

@@ -0,0 +1,224 @@
# Umbra Bookshelf — Plugin Specification
**Status:** Active
**Version:** 1.0.0
**Supersedes:** `spec/archive/SPEC_BD_Bookshelf_Plugin.txt`, `spec/archive/SPEC_BD_BookHelper_Plugin.txt`
**License:** Apache-2.0
---
## 1. Identity
| Field | Value |
|---|---|
| Plugin name | Umbra Bookshelf |
| Slug / folder | `umbra-bookshelf` |
| Entry point | `umbra-bookshelf.php` |
| Text domain | `umbra-bookshelf` |
| Domain path | `/languages` |
| Author | Umbra Tools |
| Requires | WordPress 6.0, PHP 7.4 |
| Version | 1.0.0 |
The three pre-merge plugins map onto modules as follows:
| Former plugin | Former version | Module | Code location |
|---|---|---|---|
| BD Bookshelf | 0.1.0 | **Books** (catalogue) | `includes/books/` |
| BD BookShelfHelper | 2.0.0 | **Library** | `includes/library/` |
| BD SimpleEmail | 2.0.0 | **Email** | `includes/email/` |
---
## 2. Naming rules
Two categories of identifier, treated differently on purpose.
### 2.1 Renamed to the Umbra Bookshelf identity
* Plugin header fields (`Plugin Name`, `Plugin URI`, `Author`, `Author URI`, `License`).
* Text domain: `bd-bookshelf`, `bd-bookshelfhelper`, `bd-simpleemail` → `umbra-bookshelf`.
* PHP classes: `BD_Bookshelf`, `BDM_*`, `BD_BookShelfHelper*`, `BD_SimpleEmail*`,
`BD_Email_Sender` → `Umbra_*`.
* Constants: `BD_BOOKSHELF_*` → `UMBRA_BOOKSHELF_*`; `BD_BHS_VERSION` →
`UMBRA_BOOKSHELF_LIBRARY_VERSION`; `BD_EMAIL_*` → `UMBRA_BOOKSHELF_EMAIL_VERSION`.
`BD_BHS_DIR` / `BD_EMAIL_URI` were removed in favour of the single
`UMBRA_BOOKSHELF_DIR` / `UMBRA_BOOKSHELF_URL` pair.
* Asset handles, one per module so they cannot shadow each other:
`umbra-bookshelf-admin`, `umbra-bookshelf-library-admin`,
`umbra-bookshelf-email-admin`, `umbra-bookshelf-gdpr`.
* JavaScript globals: `bdBookshelfAdmin` → `umbraBookshelfAdmin`,
`bdBookshelfGdpr` → `umbraBookshelfGdpr`, `bdmAdmin` → `umbraBookshelfLibraryAdmin`.
* Admin menu slugs (see §5).
### 2.2 Deliberately **not** renamed
These are the persisted contract with live installations and with published
post content. Renaming any of them silently orphans existing data.
| Contract | Values |
|---|---|
| Table names | `bd_books`, `bd_series`, `bd_book_authors`, `bd_authors`, `bd_book_seo`, `bd_retail_links`, `bd_retail_sites`, `bd_shortcodes`, `bd_shortcode_clicks`, `bd_book_meta`, `bd_pen_names`, `bd_reviews`, `bd_ratings`, `bd_review_shortlinks`, `bd_subscribers`, `bd_newsletters` |
| Option keys | `bd_bookshelf_db_version`, `bd_bookshelf_activation_time`, `bdm_affiliate_codes`, `bdm_goodreads_author_id`, `bdm_awards`, `bdm_gmail_address`, `bdm_gmail_password`, `bdm_sender_name`, `bdm_reply_to`, `bdm_gemini_api_key` |
| Post meta | `_bd_bookshelf_book_id` |
| Transient prefix | `_transient_bd_bookshelf_*` |
| Nonce actions | `bd_bookshelf_admin_nonce`, `bd_bookshelf_import_posts`, `bd_bookshelf_import_csv`, `bd_bookshelf_admin`, `bd_bookshelf_gdpr_nonce` |
| Cron hooks | `bd_bookshelf_sync_ratings`, `bd_bookshelf_goodreads_refresh`, `bd_bookshelf_amazon_scrape` |
| AJAX actions | `bd_bookshelf_*`, `bdm_update_links`, `bdm_autosave`, `bdm_ajax_signup`, `bdm_signup_submit`, `bdm_unsubscribe_submit` |
| Shortcodes | `[bd-book]`, `[bd-books]`, `[bdm_signup]` |
| Content types | post type `book`, taxonomy `series` |
| Cookies | `bd_bookshelf_consent` |
| CSS classes | `bd-bookshelf-*`, `bd-email-*` (paired HTML/CSS, may be styled by themes) |
One legacy PHP class name is retained by design: `class-umbra-theme-compatibility.php`
declares `Umbra_Theme_Compatibility` and then
`class_alias('Umbra_Theme_Compatibility', 'BD_BookShelfHelper')` so the
unmodified `bd-bookshelf` theme's static `BD_BookShelfHelper::` calls keep working.
---
## 3. Architecture
```
umbra-bookshelf/
├── umbra-bookshelf.php bootstrap: constants, requires, singleton, single top-level menu
├── uninstall.php drops every table and option from all three modules
├── includes/
│ ├── books/ catalogue: books, authors, series, retail, ratings,
│ │ shortcodes, SEO, GDPR, scrapers, admin, import
│ ├── library/ book post type + taxonomy, pen names, data entry,
│ │ shortlinks, reviews, ratings, migrate
│ └── email/ subscribers, newsletters, AI-assisted composition, sending
├── admin/ css, js catalogue admin assets
├── public/ css, js front-end + GDPR consent assets
├── library/ assets library admin assets
├── email/ assets email admin assets
├── assets/ images default-cover.png
├── languages/ translation catalogues
├── spec/ this document; archive/ holds pre-merge specs
├── docs/ architecture and known issues
└── tests/ verify_structure.py, smoke_runtime.php
```
**Boot order.** Modules are constructed on `plugins_loaded`, not `init`. Several
modules register their own `init` callbacks (post type, taxonomy, shortcodes,
rewrite rules); constructing them from inside an `init` callback makes it
accidental whether those callbacks run. A `$modules_initialized` flag prevents
double construction.
---
## 4. Module responsibilities
### 4.1 Books (catalogue) — `includes/books/`
Owns the custom-table catalogue model: books with ISBN-10/13 and ASIN variants,
author records and pen-name linkage, series, retail sites and per-format retail
links, Goodreads and Amazon rating synchronisation, SEO rows, shortlink
click tracking, GDPR consent, and CSV/post import.
Classes: `Umbra_Database`, `Umbra_Book_Manager`, `Umbra_Authors`, `Umbra_Series`,
`Umbra_Retail_Sites`, `Umbra_Rating_Sync`, `Umbra_Shortcodes`, `Umbra_SEO`,
`Umbra_GDPR`, `Umbra_Goodreads_Scraper`, `Umbra_Amazon_Scraper`, `Umbra_Admin`,
`Umbra_Migration`, `Umbra_Theme_Compatibility`.
> `Umbra_SEO` is never instantiated by the bootstrap (carried over unchanged from
> the original plugin). See `docs/KNOWN-ISSUES.md` item 5.
### 4.2 Library — `includes/library/`
Owns the WordPress-native model: registers post type `book` and taxonomy
`series`, pen names, a focused data-entry screen, shortlink handling, reviews and
ratings, and a migration screen for data created by earlier plugins.
Classes: `Umbra_Bookshelf_Helper`, `Umbra_Bookshelf_Helper_Admin`,
`Umbra_Bookshelf_Helper_Install`.
### 4.3 Email — `includes/email/`
Owns newsletter delivery: subscriber and newsletter tables, `[bdm_signup]`,
signup and unsubscribe form handling (AJAX and `admin_post`), Gemini-assisted
newsletter composition, and SMTP/Gmail sending.
Classes: `Umbra_Simple_Email`, `Umbra_Simple_Email_Admin`,
`Umbra_Simple_Email_Install`, `Umbra_Email_Sender`.
---
## 5. Admin menu
One top-level menu, registered exactly once by the bootstrap at
`admin_menu` priority 9. Each module contributes only submenus beneath it.
**Umbra Bookshelf** (`umbra-bookshelf`, `dashicons-book-alt`, position 30)
| Submenu slug | Module | Capability |
|---|---|---|
| `umbra-bookshelf` | bootstrap | `edit_posts` |
| `umbra-bookshelf-all` | books | `manage_options` |
| `umbra-bookshelf-add` | books | `manage_options` |
| `umbra-bookshelf-authors` | books | `manage_options` |
| `umbra-bookshelf-series` | books | `manage_options` |
| `umbra-bookshelf-sites` | books | `manage_options` |
| `umbra-bookshelf-shortcodes` | books | `manage_options` |
| `umbra-bookshelf-sync` | books | `manage_options` |
| `umbra-bookshelf-settings` | books | `manage_options` |
| `umbra-bookshelf-import` | books | `manage_options` |
| `edit.php?post_type=book` | library | `edit_posts` |
| `post-new.php?post_type=book` | library | `edit_posts` |
| `umbra-bookshelf-data-entry` | library | `edit_posts` |
| `edit-tags.php?taxonomy=series&post_type=book` | library | `manage_categories` |
| `umbra-bookshelf-pen-names` | library | `manage_options` |
| `umbra-bookshelf-library-settings` | library | `manage_options` |
| `umbra-bookshelf-migrate` | library | `manage_options` |
| `umbra-bookshelf-subscribers` | email | `manage_options` |
| `umbra-bookshelf-newsletter` | email | `manage_options` |
| `umbra-bookshelf-email-settings` | email | `manage_options` |
Catalogue and library settings are deliberately **separate** screens with
separate slugs; the pre-merge plugins both claimed `bd-bookshelf-settings`, so one
of the two was unreachable.
---
## 6. Lifecycle
* **Activate:** create catalogue tables (`Umbra_Database::create_tables()`),
library tables + roles (`Umbra_Bookshelf_Helper_Install::activate()`), email
tables (`Umbra_Simple_Email_Install::activate()`), flush rewrites, record
`bd_bookshelf_activation_time`.
* **Deactivate:** clear all three cron hooks, run module deactivators, flush
rewrites.
* **Uninstall:** drop every table across all three modules (deduplicated so
`bd_books`/`bd_series` are dropped once), delete `bd_bookshelf_%` options and
transients, delete the `bdm_*` options, clear cron. Guarded by
`WP_UNINSTALL_PLUGIN`.
---
## 7. Verification
```bash
python3 umbra-bookshelf/tests/verify_structure.py # static checks
php umbra-bookshelf/tests/smoke_runtime.php front # front-end boot
php umbra-bookshelf/tests/smoke_runtime.php admin # admin boot + menu shape
```
The static check enforces: valid PHP syntax, no class declared twice, no
reference to an undefined class, every `require_once` and enqueued asset
resolves on disk, no leftover pre-merge identifiers in shipped source, and all
preserved contracts of §2.2 still present.
The runtime check stubs WordPress, loads the bootstrap, fires `plugins_loaded`,
`init` and `admin_menu`, and asserts the post type, taxonomy and shortcodes
register, that exactly one top-level menu appears with all 20 expected submenus
under it, that no callback is registered twice on a hook, and that asset handles
are unique per module.
---
## 8. Open issues
`bd_books` and `bd_series` are created with two incompatible schemas by the
books and library modules. This predates the consolidation and is **not** fixed
here; see `docs/KNOWN-ISSUES.md`.

View File

@@ -0,0 +1,24 @@
# Archived specifications
These are the design documents for the three plugins as they existed
**before** the Umbra Bookshelf consolidation. They are kept for provenance: the
reasoning behind the original data models is not captured anywhere else.
**They describe superseded code and must not be used as an implementation
reference.** Where they disagree with `../SPEC_Umbra_Bookshelf_Plugin.md`, the
consolidated specification is correct.
| File | Describes | Status |
|---|---|---|
| `SPEC_BD_Bookshelf_Plugin.txt` | BD Bookshelf 0.1.0 (catalogue) | superseded by the Books module |
| `SPEC_BD_BookHelper_Plugin.txt` | BD BookShelfHelper 2.0.0 (library) | superseded by the Library module |
| `bd-bookshelf-plugin-spec-0.0.1.md` | BD Bookshelf design spec | superseded |
| `bd-bookshelf-plugin-questions-0.0.1.md` | design Q&A, round 1 | historical record |
| `bd-bookshelf-plugin-questions-0.0.2.md` | design Q&A, round 2 | historical record |
Old plugin names, class names and table names are preserved verbatim in these
files on purpose — they are a record of what was, not a description of what is.
The pre-merge `.docx` specifications (`BDMurphy-Plugin-Specifications-v2.2.docx`,
`BDMurphy-Theme-Specifications-v2.2.docx`) are binary and were left in the
archived **Bookshelf** repository rather than rewritten here.

View File

@@ -0,0 +1,307 @@
<?php
/**
* Runtime smoke test for the merged umbra-bookshelf plugin.
*
* Stubs just enough of WordPress to load umbra-bookshelf.php, fire the hooks
* the plugin registers on, and assert that the merged plugin boots without a
* fatal error and registers the consolidated admin menu exactly once.
*
* Run: php /tmp/opencode/smoke_umbra.php
*/
define('ABSPATH', '/tmp/fake-wp/');
define('WP_UNINSTALL_PLUGIN', 'umbra-bookshelf/umbra-bookshelf.php');
$GLOBALS['__hooks'] = array();
$GLOBALS['__menus'] = array();
$GLOBALS['__submenus'] = array();
$GLOBALS['__shortcodes'] = array();
$GLOBALS['__pt'] = array();
$GLOBALS['__tax'] = array();
$GLOBALS['__enqueued'] = array();
$GLOBALS['__options'] = array();
$GLOBALS['__cron'] = array();
$GLOBALS['__is_admin'] = (($argv[1] ?? 'front') === 'admin');
$MODE = ($argv[1] ?? 'front');
function add_action($h, $cb, $p = 10, $a = 1) { $GLOBALS['__hooks'][$h][] = $cb; }
function add_filter($h, $cb, $p = 10, $a = 1) { return true; }
function register_activation_hook($f, $cb) {}
function register_deactivation_hook($f, $cb) {}
function load_plugin_textdomain($d, $x = false, $p = '') { return true; }
function plugin_dir_path($f) { return rtrim(dirname($f), '/') . '/'; }
function plugin_dir_url($f) { return 'https://example.test/wp-content/plugins/'
. basename(dirname($f)) . '/'; }
function plugin_basename($f) { return basename(dirname($f)) . '/' . basename($f); }
function is_admin() { return $GLOBALS['__is_admin']; }
function current_user_can($c) { return true; }
function wp_die($m = '') { throw new Exception('wp_die: ' . $m); }
function __($t, $d = null) { return $t; }
function esc_html__($t, $d = null) { return $t; }
function esc_html_e($t, $d = null) { echo $t; }
function esc_attr__($t, $d = null) { return $t; }
function esc_attr_e($t, $d = null) { echo $t; }
function esc_html($t) { return $t; }
function esc_url($t) { return $t; }
function esc_textarea($t) { return $t; }
function number_format_i18n($n) { return number_format($n); }
function absint($n) { return abs((int) $n); }
function sanitize_text_field($t) { return $t; }
function sanitize_key($t) { return preg_replace('/[^a-z0-9_\-]/', '', strtolower($t)); }
function wp_create_nonce($a) { return 'nonce'; }
function wp_verify_nonce($n, $a) { return 1; }
function admin_url($p = '') { return 'https://example.test/wp-admin/' . $p; }
function flush_rewrite_rules($h = true) {}
function update_option($k, $v, $a = null) { $GLOBALS['__options'][$k] = $v; return true; }
function get_option($k, $d = false) { return $GLOBALS['__options'][$k] ?? $d; }
function delete_option($k) { unset($GLOBALS['__options'][$k]); return true; }
function wp_count_posts($t) { return (object) array('publish' => 7); }
function get_terms($a = array()) { return 4; }
function is_wp_error($t) { return false; }
function get_post_meta($p, $k, $s = true) { return ''; }
function update_post_meta($p, $k, $v) { return true; }
function get_post($p) { return null; }
function get_the_post_thumbnail_url($p, $s = '') { return ''; }
function wp_schedule_event($t, $r, $h) { $GLOBALS['__cron'][$h] = true; }
function wp_next_scheduled($h) { return false; }
function wp_clear_scheduled_hook($h) { unset($GLOBALS['__cron'][$h]); }
function wp_unschedule_hook($h) {}
function add_shortcode($tag, $cb) { $GLOBALS['__shortcodes'][] = $tag; }
function register_post_type($t, $a = array()) { $GLOBALS['__pt'][$t] = $a; return $t; }
function register_taxonomy($t, $o, $a = array()) { $GLOBALS['__tax'][$t] = $a; return $t; }
function add_menu_page($pt, $mt, $cap, $slug, $cb = '', $icon = '', $pos = null) {
$GLOBALS['__menus'][$slug] = array('page_title' => $pt, 'capability' => $cap,
'callback' => $cb, 'icon' => $icon, 'position' => $pos);
return $slug;
}
function add_submenu_page($parent, $pt, $mt, $cap, $slug, $cb = '') {
$GLOBALS['__submenus'][$slug] = array('parent' => $parent, 'page_title' => $pt,
'menu_title' => $mt, 'capability' => $cap,
'callback' => $cb);
return $slug;
}
function get_current_screen() { return null; }
function wp_enqueue_media() {}
function wp_enqueue_style($h, $src = '', $d = array(), $v = false, $m = 'all') {
$GLOBALS['__enqueued']['style'][$h] = $src;
}
function wp_enqueue_script($h, $src = '', $d = array(), $v = false, $f = false) {
$GLOBALS['__enqueued']['script'][$h] = $src;
}
function wp_localize_script($h, $name, $data) {
$GLOBALS['__enqueued']['localize'][$h] = $name;
}
function wp_register_post_type_args($a, $t) { return $a; }
function register_post_meta_args($a, $t) { return $a; }
function wp_get_current_user() { return null; }
function get_userdata($i) { return false; }
function wp_roles() { return null; }
function add_role($r, $n, $c) {}
function remove_role($r) {}
function wp_send_json_success($d) {}
function wp_send_json_error($d) {}
function check_ajax_referer($a, $q = false) { return 1; }
function wp_die_ajax($m) {}
function sanitize_email($e) { return $e; }
function sanitize_key_($k) { return $k; }
function get_home_url($p = '') { return 'https://example.test'; }
function home_url($p = '') { return 'https://example.test'; }
function wp_mail($to, $subj, $msg) { return true; }
function wp_rand($a = 0, $b = 0) { return 12345; }
function get_bloginfo($s = '') { return 'Example'; }
function get_user_meta($i, $k, $s = false) { return ''; }
function update_user_meta($i, $k, $v) { return true; }
function add_rewrite_rule($r, $q, $after = 'bottom') { return true; }
function add_rewrite_tag($r, $q, $a = '') { return true; }
function add_query_var($v) { return $v; }
function get_query_var($v, $d = '') { return $d; }
function get_option_blogname($d = '') { return 'Example'; }
function do_action($h) {
foreach ($GLOBALS['__hooks'][$h] ?? array() as $cb) {
if (is_callable($cb)) { call_user_func($cb); }
}
}
$fail = 0;
function check($label, $cond, $detail = '') {
global $fail;
if ($cond) {
echo " ok $label\n";
} else {
echo " FAIL $label" . ($detail ? " ($detail)" : '') . "\n";
$GLOBALS['fail']++;
}
}
function section($t) { echo "\n$t\n"; }
// ---------------------------------------------------------------- load it
section('1. Load plugin bootstrap (front-end context)');
try {
require dirname(__DIR__) . '/umbra-bookshelf.php';
echo " ok bootstrap included without fatal error\n";
} catch (Throwable $e) {
echo " FAIL fatal: " . $e->getMessage() . "\n";
exit(1);
}
section('2. Core class graph');
check('Umbra_Bookshelf class exists', class_exists('Umbra_Bookshelf'));
check('singleton instance() returns object',
umbra_bookshelf() instanceof Umbra_Bookshelf);
check('instance() is idempotent',
umbra_bookshelf() === umbra_bookshelf());
section('3. Module classes loaded');
foreach (array('Umbra_Database', 'Umbra_Book_Manager', 'Umbra_Series',
'Umbra_Authors', 'Umbra_Retail_Sites', 'Umbra_Rating_Sync',
'Umbra_Shortcodes', 'Umbra_SEO', 'Umbra_GDPR',
'Umbra_Goodreads_Scraper', 'Umbra_Amazon_Scraper',
'Umbra_Admin', 'Umbra_Migration', 'Umbra_Theme_Compatibility',
'Umbra_Bookshelf_Helper', 'Umbra_Bookshelf_Helper_Admin',
'Umbra_Bookshelf_Helper_Install', 'Umbra_Simple_Email',
'Umbra_Simple_Email_Admin', 'Umbra_Simple_Email_Install',
'Umbra_Email_Sender') as $c) {
check("class $c", class_exists($c));
}
section('4. Legacy theme alias');
check('class_alias exposes BD_BookShelfHelper for the theme',
class_exists('BD_BookShelfHelper', false));
check('alias resolves to the compatibility shim',
is_a('BD_BookShelfHelper', 'Umbra_Theme_Compatibility', true));
check('shim exposes get_cover_url (theme API)',
method_exists('BD_BookShelfHelper', 'get_cover_url'));
check('real library class still distinct',
!is_a('Umbra_Bookshelf_Helper', 'Umbra_Theme_Compatibility', true));
section('5. Constants');
check('UMBRA_BOOKSHELF_VERSION', defined('UMBRA_BOOKSHELF_VERSION')
&& UMBRA_BOOKSHELF_VERSION === '1.0.0', UMBRA_BOOKSHELF_VERSION ?? 'undefined');
check('UMBRA_BOOKSHELF_DIR', defined('UMBRA_BOOKSHELF_DIR'));
check('UMBRA_BOOKSHELF_URL', defined('UMBRA_BOOKSHELF_URL'));
check('UMBRA_BOOKSHELF_BASENAME', defined('UMBRA_BOOKSHELF_BASENAME'));
check('library module version', defined('UMBRA_BOOKSHELF_LIBRARY_VERSION'));
check('email module version', defined('UMBRA_BOOKSHELF_EMAIL_VERSION'));
check('old BD_BOOKSHELF_VERSION removed', !defined('BD_BOOKSHELF_VERSION'));
check('old BD_BHS_DIR removed', !defined('BD_BHS_DIR'));
check('old BD_EMAIL_URI removed', !defined('BD_EMAIL_URI'));
section('6. Boot the modules (' . $MODE . ' request)');
try {
do_action('plugins_loaded');
do_action('init');
echo " ok plugins_loaded + init fired without fatal error\n";
} catch (Throwable $e) {
echo " FAIL fatal on init: " . $e->getMessage() . "\n";
$GLOBALS['fail']++;
}
check('book post type registered by library module',
isset($GLOBALS['__pt']['book']));
check('series taxonomy registered by library module',
isset($GLOBALS['__tax']['series']));
check('[bd-book] shortcode preserved',
in_array('bd-book', $GLOBALS['__shortcodes'], true));
check('[bd-books] shortcode preserved',
in_array('bd-books', $GLOBALS['__shortcodes'], true));
check('[bdm_signup] shortcode preserved',
in_array('bdm_signup', $GLOBALS['__shortcodes'], true));
section('7. Admin menu consolidation (admin context)');
try {
do_action('admin_menu');
echo " ok admin_menu hooks fired without fatal error\n";
} catch (Throwable $e) {
echo " FAIL fatal on admin_menu: " . $e->getMessage() . "\n";
$GLOBALS['fail']++;
}
$menus = $GLOBALS['__menus'];
if ($MODE !== 'admin') {
echo " .. skipped menu assertions (front-end request: no admin_menu)\n";
section('RESULT');
if (!empty($GLOBALS['fail'])) {
echo "FAIL - {$GLOBALS['fail']} check(s) failed\n";
exit(1);
}
echo "PASS - runtime smoke test clean\n";
exit(0);
}
check('exactly ONE top-level menu registered', count($menus) === 1,
count($menus) . ': ' . implode(', ', array_keys($menus)));
check('top-level slug is umbra-bookshelf',
isset($menus['umbra-bookshelf']), implode(', ', array_keys($menus)));
check('top-level titled "Umbra Bookshelf"',
($menus['umbra-bookshelf']['page_title'] ?? '') === 'Umbra Bookshelf',
$menus['umbra-bookshelf']['page_title'] ?? '');
$subs = $GLOBALS['__submenus'];
$badParent = array();
foreach ($subs as $slug => $s) {
if ($s['parent'] !== 'umbra-bookshelf') { $badParent[$slug] = $s['parent']; }
}
check('every submenu hangs off umbra-bookshelf', count($badParent) === 0,
json_encode($badParent));
$expected = array(
'umbra-bookshelf', 'umbra-bookshelf-all', 'umbra-bookshelf-add',
'umbra-bookshelf-authors', 'umbra-bookshelf-series', 'umbra-bookshelf-sites',
'umbra-bookshelf-shortcodes', 'umbra-bookshelf-sync', 'umbra-bookshelf-settings',
'umbra-bookshelf-import',
'edit.php?post_type=book', 'post-new.php?post_type=book',
'umbra-bookshelf-data-entry', 'edit-tags.php?taxonomy=series&post_type=book',
'umbra-bookshelf-pen-names', 'umbra-bookshelf-library-settings',
'umbra-bookshelf-migrate',
'umbra-bookshelf-subscribers', 'umbra-bookshelf-newsletter',
'umbra-bookshelf-email-settings',
);
$missing = array_values(array_diff($expected, array_keys($subs)));
check('all ' . count($expected) . ' expected submenus present', count($missing) === 0,
'missing: ' . implode(', ', $missing));
check('no duplicate submenu slug (settings screens distinct)',
!isset($subs['bd-bookshelf-settings']),
'legacy duplicate slug still registered');
check('catalogue settings present', isset($subs['umbra-bookshelf-settings']));
check('library settings present', isset($subs['umbra-bookshelf-library-settings']));
check('email settings present', isset($subs['umbra-bookshelf-email-settings']));
section('8. Duplicate hook / action registrations');
$dupes = array();
foreach ($GLOBALS['__hooks'] as $hook => $cbs) {
$seen = array();
foreach ($cbs as $cb) {
$k = is_string($cb) ? $cb : (is_array($cb) ? (is_object($cb[0]) ? get_class($cb[0]) . '::' . $cb[1] : $cb[0] . '::' . $cb[1]) : 'closure');
$seen[$k] = ($seen[$k] ?? 0) + 1;
}
foreach ($seen as $k => $n) {
if ($n > 1) { $dupes[] = "$hook => $k ($n)"; }
}
}
check('no callback registered twice on one hook', count($dupes) === 0,
implode('; ', $dupes));
section('9. Asset handles are unique per module');
$styles = array_keys($GLOBALS['__enqueued']['style'] ?? array());
$scripts = array_keys($GLOBALS['__enqueued']['script'] ?? array());
check('no duplicated style handle', count($styles) === count(array_unique($styles)),
implode(', ', $styles));
check('no duplicated script handle', count($scripts) === count(array_unique($scripts)),
implode(', ', $scripts));
section('10. Uninstall routine parses and targets real tables');
$src = file_get_contents(dirname(__DIR__) . '/uninstall.php');
check('uninstall references catalogue drop_tables', strpos($src, 'drop_tables()') !== false);
foreach (array('bd_book_meta', 'bd_pen_names', 'bd_reviews', 'bd_ratings',
'bd_review_shortlinks', 'bd_subscribers', 'bd_newsletters') as $t) {
check("uninstall drops $t", strpos($src, $t) !== false);
}
check('uninstall clears all three cron hooks',
substr_count($src, 'wp_clear_scheduled_hook') === 3,
substr_count($src, 'wp_clear_scheduled_hook') . ' calls');
section('RESULT');
if (!empty($GLOBALS['fail'])) {
echo "FAIL - {$GLOBALS['fail']} check(s) failed\n";
exit(1);
}
echo "PASS - runtime smoke test clean\n";

View File

@@ -0,0 +1,180 @@
#!/usr/bin/env python3
"""Static verification for the merged umbra-bookshelf plugin.
Checks:
1. PHP syntax of every file
2. No class declared twice (fatal error in PHP)
3. Every class referenced by new X / X:: actually exists
4. Every require_once path resolves on disk
5. Every wp_enqueue_* asset path resolves on disk (no 404s)
6. No leftover pre-rename identifiers
7. Preserved storage/API contracts still present
"""
import io
import os
import re
import subprocess
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
failures = []
notes = []
def rel(p):
return os.path.relpath(p, ROOT)
def walk(exts):
for dirpath, _, files in os.walk(ROOT):
for f in files:
if f.endswith(exts):
yield os.path.join(dirpath, f)
# ---------------------------------------------------------------- 1. php -l
php = [f for f in walk('.php')]
for f in php:
r = subprocess.run(['php', '-l', f], capture_output=True, text=True)
if 'No syntax errors' not in r.stdout:
failures.append('PHP SYNTAX: %s\n%s' % (rel(f), r.stdout + r.stderr))
print('1. php -l: %d files checked' % len(php))
# ------------------------------------------------- 2. duplicate class decls
decls = {}
for f in php:
src = io.open(f, encoding='utf-8', errors='replace').read()
for m in re.finditer(r'^\s*(?:final\s+|abstract\s+)?class\s+([A-Za-z_][A-Za-z0-9_]*)',
src, re.M):
decls.setdefault(m.group(1), []).append(rel(f))
dupes = {c: fs for c, fs in decls.items() if len(fs) > 1}
for c, fs in sorted(dupes.items()):
failures.append('DUPLICATE CLASS: %s declared in %s' % (c, ', '.join(fs)))
print('2. class declarations: %d unique, %d duplicated'
% (len(decls), len(dupes)))
# --------------------------------------------- 3. referenced classes exist
refs = set()
for f in php:
src = io.open(f, encoding='utf-8', errors='replace').read()
for m in re.finditer(r'\bnew\s+([A-Z][A-Za-z0-9_]*)\s*\(', src):
refs.add(m.group(1))
for m in re.finditer(r"\b([A-Z][A-Za-z0-9_]*)::", src):
refs.add(m.group(1))
# names defined by this plugin, or provided by WordPress
wp_provided = {'BD_BookShelfHelper', # created via class_alias()
'WP_Error', 'WP_Query', 'WP_Post', 'WP_REST_Response',
'WP_REST_Request', 'WP_REST_Server', 'WP_Term', 'wpdb',
'ReflectionClass', 'DateTime', 'Exception', 'ArrayObject'}
known_external = {'GeminiClient',
# browser JS globals, matched by the `new X(` heuristic
# when scanning inline <script> blocks inside PHP files
'FormData', 'URL', 'Image', 'Audio'}
missing = sorted(r for r in refs
if r not in decls
and r not in wp_provided
and r not in known_external)
for r in missing:
failures.append('UNDEFINED CLASS REFERENCED: %s' % r)
print('3. class references: %d referenced, %d undefined' % (len(refs), len(missing)))
# --------------------------------------------------- 4. require_once paths
for f in php:
src = io.open(f, encoding='utf-8', errors='replace').read()
for m in re.finditer(r"require(?:_once)?\s+([^;]+);", src):
expr = m.group(1)
pm = re.search(r"UMBRA_BOOKSHELF_DIR\s*\.\s*'([^']+)'", expr)
if pm:
target = os.path.join(ROOT, pm.group(1).lstrip('/'))
if not os.path.isfile(target):
failures.append('MISSING REQUIRE: %s -> %s' % (rel(f), pm.group(1)))
elif "__DIR__" in expr:
pm = re.search(r"__DIR__\s*\.\s*'([^']+)'", expr)
if pm:
target = os.path.join(os.path.dirname(f), pm.group(1).lstrip('/'))
if not os.path.isfile(target):
failures.append('MISSING REQUIRE: %s -> %s'
% (rel(f), pm.group(1)))
print('4. require_once targets verified')
# ------------------------------------------------------- 5. asset 404 check
assets = 0
for f in list(walk('.php')):
src = io.open(f, encoding='utf-8', errors='replace').read()
for m in re.finditer(
r"UMBRA_BOOKSHELF_URL\s*\.\s*'([^']+\.(?:css|js|png|jpg|svg))'",
src):
target = os.path.join(ROOT, m.group(1))
assets += 1
if not os.path.isfile(target):
failures.append('ASSET 404: %s -> %s' % (rel(f), m.group(1)))
print('5. enqueued assets: %d references, all resolved' % assets)
# -------------------------------------------- 6. leftover pre-rename tokens
stale_pat = re.compile(r'\b(?:BD_[A-Z][A-Za-z0-9_]*|BDM_[A-Za-z0-9_]*)\b')
stale = []
# Scan shipped plugin source only. Excluded on purpose:
# spec/archive/ -- historical pre-merge specs, must keep the old names
# tests/ -- asserts that the old names are GONE, so it mentions them
SCAN_DIRS = ('includes', 'admin', 'public', 'library', 'email')
scan_files = []
for d in SCAN_DIRS:
for dirpath, _, files in os.walk(os.path.join(ROOT, d)):
for f in files:
if f.endswith(('.php', '.js', '.css')):
scan_files.append(os.path.join(dirpath, f))
for f in ['umbra-bookshelf.php', 'uninstall.php']:
scan_files.append(os.path.join(ROOT, f))
scan_files = [f for f in scan_files if os.path.isfile(f)]
for f in scan_files:
src = io.open(f, encoding='utf-8', errors='replace').read()
for i, line in enumerate(src.splitlines(), 1):
for m in stale_pat.finditer(line):
# BD_BookShelfHelper is intentionally kept as the legacy theme alias
if m.group(0) == 'BD_BookShelfHelper':
notes.append('%s:%d keeps legacy alias %s'
% (rel(f), i, m.group(0)))
continue
stale.append('%s:%d %s' % (rel(f), i, m.group(0)))
for s in sorted(set(stale)):
failures.append('STALE IDENTIFIER: %s' % s)
print('6. stale identifiers: %d (excluding documented legacy alias)'
% len(set(stale)))
# ------------------------------------- 7. preserved contracts still present
contracts = [
('bd_books', 'table name'),
('bd_series', 'table name'),
('bd_bookshelf_db_version', 'option key'),
('bd_bookshelf_activation_time', 'option key'),
('_bd_bookshelf_book_id', 'post meta key'),
('bd_bookshelf_import_posts', 'nonce action'),
('bd_bookshelf_import_csv', 'nonce/ajax action'),
('bd_bookshelf_sync_ratings', 'cron hook'),
('bd_bookshelf_goodreads_refresh', 'cron hook'),
('bd_bookshelf_amazon_scrape', 'cron hook'),
('bdm_update_links', 'ajax action'),
('bdm_autosave', 'ajax action'),
('bdm_ajax_signup', 'ajax action'),
('bd_bookshelf_consent', 'cookie name'),
]
blob = ''
for f in walk(('.php', '.js')):
blob += io.open(f, encoding='utf-8', errors='replace').read()
for tok, kind in contracts:
if tok not in blob:
failures.append('LOST CONTRACT (%s): %s' % (kind, tok))
print('7. preserved contracts: %d checked' % len(contracts))
# ------------------------------------------------------------------- report
print()
for n in sorted(set(notes)):
print('note: %s' % n)
print()
if failures:
print('FAIL (%d)' % len(failures))
for f in failures:
print(' - %s' % f)
sys.exit(1)
print('PASS - all static checks clean')

View File

@@ -0,0 +1,318 @@
<?php
// Copyright 2026 Umbra Tools (umbratools.com)
// SPDX-License-Identifier: Apache-2.0
/**
* Plugin Name: Umbra Bookshelf
* Plugin URI: https://umbratools.com/plugins/umbra-bookshelf
* Description: Complete book management for authors and publishers: catalogue and ISBN tracking, series and pen names, retail links, rating synchronisation, GDPR consent, data entry, migration tooling, and newsletter delivery.
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 7.4
* Author: Umbra Tools
* Author URI: https://umbratools.com
* License: Apache-2.0
* License URI: https://www.apache.org/licenses/LICENSE-2.0
* Text Domain: umbra-bookshelf
* Domain Path: /languages
*
* Umbra Bookshelf is the consolidation of three previously separate plugins:
* - BD Bookshelf -> includes/books
* - BD BookShelfHelper -> includes/library
* - BD SimpleEmail -> includes/email
*
* The top-level admin menu is registered exactly once, here. Each module
* contributes only submenus beneath the shared "umbra-bookshelf" parent.
*
* @package Umbra_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* Plugin constants
*/
define('UMBRA_BOOKSHELF_VERSION', '1.0.0');
define('UMBRA_BOOKSHELF_FILE', __FILE__);
define('UMBRA_BOOKSHELF_DIR', plugin_dir_path(__FILE__));
define('UMBRA_BOOKSHELF_URL', plugin_dir_url(__FILE__));
define('UMBRA_BOOKSHELF_BASENAME', plugin_basename(__FILE__));
/**
* Module version constants
*
* Storage keys (table names, option names, meta keys, cron hooks, nonces and
* shortcodes) are deliberately NOT renamed. They are the persisted contract
* with existing installations and with published post content.
*/
define('UMBRA_BOOKSHELF_LIBRARY_VERSION', '2.0.0');
define('UMBRA_BOOKSHELF_EMAIL_VERSION', '2.0.0');
/**
* Load module files
*/
// Books / catalogue module (ex-BD Bookshelf)
require_once UMBRA_BOOKSHELF_DIR . 'includes/books/class-umbra-database.php';
require_once UMBRA_BOOKSHELF_DIR . 'includes/books/class-umbra-book-manager.php';
require_once UMBRA_BOOKSHELF_DIR . 'includes/books/class-umbra-authors.php';
require_once UMBRA_BOOKSHELF_DIR . 'includes/books/class-umbra-series.php';
require_once UMBRA_BOOKSHELF_DIR . 'includes/books/class-umbra-retail-sites.php';
require_once UMBRA_BOOKSHELF_DIR . 'includes/books/class-umbra-rating-sync.php';
require_once UMBRA_BOOKSHELF_DIR . 'includes/books/class-umbra-shortcodes.php';
require_once UMBRA_BOOKSHELF_DIR . 'includes/books/class-umbra-seo.php';
require_once UMBRA_BOOKSHELF_DIR . 'includes/books/class-umbra-gdpr.php';
require_once UMBRA_BOOKSHELF_DIR . 'includes/books/class-umbra-goodreads-scraper.php';
require_once UMBRA_BOOKSHELF_DIR . 'includes/books/class-umbra-amazon-scraper.php';
require_once UMBRA_BOOKSHELF_DIR . 'includes/books/class-umbra-admin.php';
require_once UMBRA_BOOKSHELF_DIR . 'includes/books/class-umbra-migration.php';
require_once UMBRA_BOOKSHELF_DIR . 'includes/books/class-umbra-theme-compatibility.php';
// Library module (ex-BD BookShelfHelper)
require_once UMBRA_BOOKSHELF_DIR . 'includes/library/class-umbra-bookshelf-helper.php';
require_once UMBRA_BOOKSHELF_DIR . 'includes/library/class-umbra-library-admin.php';
require_once UMBRA_BOOKSHELF_DIR . 'includes/library/class-umbra-library-install.php';
// Email module (ex-BD SimpleEmail)
require_once UMBRA_BOOKSHELF_DIR . 'includes/email/class-umbra-simple-email.php';
require_once UMBRA_BOOKSHELF_DIR . 'includes/email/class-umbra-email-admin.php';
require_once UMBRA_BOOKSHELF_DIR . 'includes/email/class-umbra-email-install.php';
require_once UMBRA_BOOKSHELF_DIR . 'includes/email/class-umbra-email-sender.php';
/**
* Main plugin class
*/
final class Umbra_Bookshelf {
/**
* Single instance of the class
*
* @var Umbra_Bookshelf|null
*/
private static $instance = null;
/**
* Top-level admin menu slug. Shared by every module.
*/
const MENU_SLUG = 'umbra-bookshelf';
/**
* Guard so modules are never constructed twice in one request.
*
* @var bool
*/
private $modules_initialized = false;
/**
* Get single instance
*
* @return Umbra_Bookshelf
*/
public static function instance() {
if (is_null(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
$this->init_hooks();
}
/**
* Initialize hooks
*/
private function init_hooks() {
register_activation_hook(UMBRA_BOOKSHELF_FILE, [$this, 'activate']);
register_deactivation_hook(UMBRA_BOOKSHELF_FILE, [$this, 'deactivate']);
add_action('init', [$this, 'load_textdomain']);
add_action('admin_menu', [$this, 'register_admin_menu'], 9);
// Modules MUST be constructed on plugins_loaded, not init. Several of
// them register their own 'init' callbacks (post type, taxonomy,
// shortcodes, rewrite rules). Constructing them from an init callback
// means those init callbacks are added while 'init' is already being
// dispatched, so whether they run becomes an ordering accident.
add_action('plugins_loaded', [$this, 'init_modules']);
}
/**
* Load translations
*/
public function load_textdomain() {
load_plugin_textdomain(
'umbra-bookshelf',
false,
dirname(UMBRA_BOOKSHELF_BASENAME) . '/languages'
);
}
/**
* Register the single top-level admin menu.
*
* Runs at priority 9 so the parent exists before any module appends its
* submenus at the default priority 10.
*/
public function register_admin_menu() {
add_menu_page(
__('Umbra Bookshelf', 'umbra-bookshelf'),
__('Umbra Bookshelf', 'umbra-bookshelf'),
'edit_posts',
self::MENU_SLUG,
[$this, 'render_dashboard'],
'dashicons-book-alt',
30
);
// First entry is conventionally a copy of the parent, pointing at the
// catalogue overview.
add_submenu_page(
self::MENU_SLUG,
__('Dashboard', 'umbra-bookshelf'),
__('Dashboard', 'umbra-bookshelf'),
'edit_posts',
self::MENU_SLUG,
[$this, 'render_dashboard']
);
}
/**
* Render the dashboard / catalogue overview.
*/
public function render_dashboard() {
if (!current_user_can('edit_posts')) {
wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'umbra-bookshelf'));
}
$counts = array(
'books' => $this->count_books(),
'series' => $this->count_series(),
);
?>
<div class="wrap">
<h1><?php esc_html_e('Umbra Bookshelf', 'umbra-bookshelf'); ?></h1>
<div class="umbra-bookshelf-dashboard">
<div class="umbra-bookshelf-dashboard-card">
<h2><?php esc_html_e('Books', 'umbra-bookshelf'); ?></h2>
<p class="umbra-bookshelf-dashboard-count"><?php echo esc_html(number_format_i18n($counts['books'])); ?></p>
</div>
<div class="umbra-bookshelf-dashboard-card">
<h2><?php esc_html_e('Series', 'umbra-bookshelf'); ?></h2>
<p class="umbra-bookshelf-dashboard-count"><?php echo esc_html(number_format_i18n($counts['series'])); ?></p>
</div>
</div>
</div>
<?php
}
/**
* Number of books visible to the current user.
*
* @return int
*/
private function count_books() {
$counts = wp_count_posts('book');
return isset($counts->publish) ? (int) $counts->publish : 0;
}
/**
* Number of series terms.
*
* @return int
*/
private function count_series() {
$terms = get_terms(array(
'taxonomy' => 'series',
'hide_empty' => false,
'fields' => 'count',
));
return is_wp_error($terms) ? 0 : (int) $terms;
}
/**
* Initialize modules
*/
public function init_modules() {
if ($this->modules_initialized) {
return;
}
$this->modules_initialized = true;
// Catalogue module
new Umbra_Shortcodes();
new Umbra_GDPR();
new Umbra_Rating_Sync();
// Library module (post type, taxonomy, shortcodes, pen names)
Umbra_Bookshelf_Helper::instance();
// Email module (signup, unsubscribe, newsletter delivery)
Umbra_Simple_Email::instance();
if (is_admin()) {
new Umbra_Admin();
new Umbra_Migration();
Umbra_Bookshelf_Helper_Admin::instance();
Umbra_Simple_Email_Admin::instance();
}
}
/**
* Plugin activation
*
* Runs each module's installer. See docs/KNOWN-ISSUES.md item 1: the
* catalogue and library modules both create bd_books / bd_series with
* different schemas, which is a pre-existing conflict carried over from
* the separate plugins and deliberately not resolved in this release.
*/
public function activate() {
$database = new Umbra_Database();
$database->create_tables();
Umbra_Bookshelf_Helper_Install::activate();
Umbra_Simple_Email_Install::activate();
flush_rewrite_rules();
// Preserved option key (existing installations already have this).
update_option('bd_bookshelf_activation_time', current_time('timestamp'));
}
/**
* Plugin deactivation
*/
public function deactivate() {
// Preserved cron hook names.
wp_clear_scheduled_hook('bd_bookshelf_sync_ratings');
wp_clear_scheduled_hook('bd_bookshelf_goodreads_refresh');
wp_clear_scheduled_hook('bd_bookshelf_amazon_scrape');
Umbra_Bookshelf_Helper_Install::deactivate();
Umbra_Simple_Email_Install::deactivate();
flush_rewrite_rules();
}
}
/**
* Initialize plugin
*
* @return Umbra_Bookshelf
*/
function umbra_bookshelf() {
return Umbra_Bookshelf::instance();
}
// Start the plugin
umbra_bookshelf();

View File

@@ -0,0 +1,71 @@
<?php
// Copyright 2026 Umbra Tools (umbratools.com)
// SPDX-License-Identifier: Apache-2.0
/**
* Umbra Bookshelf Uninstall
*
* Fired when the plugin is deleted from the Plugins page. Removes every table
* and option owned by all three consolidated modules (books, library, email).
*
* Table names and option keys are the historical bd_* keys and are preserved
* verbatim -- that is what the data on disk is actually called.
*
* @package Umbra_Bookshelf
*/
if (!defined('WP_UNINSTALL_PLUGIN')) {
exit;
}
// Include database class
require_once __DIR__ . '/includes/books/class-umbra-database.php';
// Drop catalogue tables (bd_books, bd_series, bd_book_authors, ...)
$database = new Umbra_Database();
$database->drop_tables();
// Drop library and email tables.
// bd_books and bd_series are intentionally NOT repeated here: the catalogue
// module's drop_tables() above already covers them, and dropping the same table
// twice is only a wasted query.
global $wpdb;
$additional_tables = array(
// Library module
$wpdb->prefix . 'bd_book_meta',
$wpdb->prefix . 'bd_pen_names',
$wpdb->prefix . 'bd_reviews',
$wpdb->prefix . 'bd_ratings',
$wpdb->prefix . 'bd_review_shortlinks',
// Email module
$wpdb->prefix . 'bd_subscribers',
$wpdb->prefix . 'bd_newsletters',
);
foreach ($additional_tables as $table) {
$wpdb->query("DROP TABLE IF EXISTS {$table}");
}
// Remove catalogue options and transients (preserved bd_bookshelf_* keys)
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE 'bd_bookshelf_%'");
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_bd_bookshelf_%'");
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_bd_bookshelf_%'");
// Remove library options
delete_option('bdm_affiliate_codes');
delete_option('bdm_goodreads_author_id');
delete_option('bdm_awards');
// Remove email options
delete_option('bdm_gmail_address');
delete_option('bdm_gmail_password');
delete_option('bdm_sender_name');
delete_option('bdm_reply_to');
delete_option('bdm_gemini_api_key');
// Clear scheduled events (preserved hook names)
wp_clear_scheduled_hook('bd_bookshelf_sync_ratings');
wp_clear_scheduled_hook('bd_bookshelf_goodreads_refresh');
wp_clear_scheduled_hook('bd_bookshelf_amazon_scrape');