Track Bookshelf WordPress theme, plugins and specs

Initial import of the bdmurphy Bookshelf work: the bd-bookshelf theme,
bd-bookshelf-plugin, bd-bookshelfhelper and bd-simpleemail plugins, plus
the specification documents, mockups and release archives.
This commit is contained in:
bdm
2026-09-28 06:24:25 -07:00
commit 38cdebd0c6
217 changed files with 30197 additions and 0 deletions

22
.gitignore vendored Normal file
View File

@@ -0,0 +1,22 @@
# OS / editor cruft
.DS_Store
Thumbs.db
*.swp
*~
.idea/
# Local environment / secrets
.env
.env.*
*.pem
*.key
!book_retailer_icons/*.png
# Build / dependency artifacts
node_modules/
vendor/
*.min.js
*.min.css
# Logs
*.log

3
.vscode/settings.json vendored Executable file
View File

@@ -0,0 +1,3 @@
{
"local.model.provider.serverUrl": "http://192.168.123.14:1234"
}

44
Amazon_View_style_module.md Executable file
View File

@@ -0,0 +1,44 @@
Book layout section:
Similar to an Amazon book detail page.
The language selection will be followed for the page. If there is no translation for a language to show then English will be shown.
On the left is the book cover image, sized appropriatly for the display size.
To the right of the cover image, at the top is the book title, large as a title.
The line below the title will have "by" followed by the author name, or pen name.
The next line will show the Amazon rating, with space and the goodreads rating for the book.
There will be a seperator line below the ratings
Then the back cover blurb will be displayed.
Below the cover image on the left will be buttons linking to retailers to purchase the book.
The top buttons will be the formats available (ebook, paperback, hardback, audio). Below the format selection will be links to the sites with that format. The sites which don't offer that format will be grayed out.
To the right of the button, under the blurb will be additional book information, Genre, length in pages, language selected (with a '+' if other languages are availasble.) and the publisher name.
Below this line there will be the book ASIC listing categories.
SYSTEM INSTRUCTION: Generate book listing components for WordPress using the
"Amazon Detail" layout structure.
LAYOUT RULES:
1. CONTAINER: Each book must be wrapped in a container that supports a two-
column layout.
2. LEFT COLUMN (Width 25%):
- TOP: Display the book cover image (Responsive sizing).
- BOTTOM: Display Purchase Section.
- Row 1: Format selectors (Ebook, Paperback, Hardback, Audio).
- Row 2: List of retailers. Apply CSS class "grayed-out" to retailers
that do not support the selected format.
3. RIGHT COLUMN (Width 75%):
- TITLE: Display in Large Bold H1/H2 style.
- BYLINE: "by [Author/Pen Name]" in secondary accent color.
- RATINGS: Show "Amazon: [Stars] | Goodreads: [Score]" on one line.
- SEPARATOR: Horizontal rule line.
- BLURB: Full back cover description text.
- METADATA BLOCK: Below blurb, display "Genre | [Length] Pages |
[Language] (+) | [Publisher]".
- CATEGORIES: Display ASIC listing categories in small muted text at the
very bottom.
4. LANGUAGE LOGIC: Default to selected language. If translation is missing,
fall back to English.

View File

@@ -0,0 +1,912 @@
# BDMurphy BookShelf Template - Technical Specification v2.0
**WordPress Theme:** BDMurphy BookShelf
**Plugin 1:** BD-BookShelfHelper
**Plugin 2:** BD-SimpleEmail
**Version:** 2.0 | April 2026
**Updated:** Incorporates WordPress Coding Standards, Security Best Practices, Accessibility Requirements, and Testing Standards
---
## Table of Contents
1. [Project Overview](#1-project-overview)
2. [Theme - Global Layout](#2-theme---global-layout)
3. [Theme - Pages](#3-theme---pages)
4. [Design System](#4-design-system)
5. [Plugin: BD-BookShelfHelper](#5-plugin-bd-bookshelfhelper)
6. [Plugin: BD-SimpleEmail](#6-plugin-bd-simpleemail)
7. [Database Schema](#7-database-schema)
8. [Design Decisions](#8-design-decisions)
9. [Implementation Standards](#9-implementation-standards)
10. [Quality Requirements](#10-quality-requirements)
11. [Deployment Checklist](#11-deployment-checklist)
---
## 1. Project Overview
### 1.1 Purpose
This specification defines a custom WordPress theme and two companion plugins for author B.D. Murphy's website. The system allows the author to showcase books, publish blogs, manage reviews, and send newsletters with minimal technical knowledge required after initial setup.
**Theme Name:** BDMurphy Bookshelf
**Plugin 1:** BD-BookShelfHelper
**Plugin 2:** BD-SimpleEmail
### 1.2 Deliverables
- **WordPress Theme:** BDMurphy Bookshelf — custom theme, fully Customizer-driven
- **Plugin: BD BookShelfHelper:** Book data, series, pen names, short links, review links, SEO, and platform integrations. Book data stored and managed in one place accessable everywhere.
- **Plugin: BD SimpleEmail:** Subscriber management and AI-assisted newsletter delivery
### 1.3 Design Identity
| Element | Specification |
|---------|--------------|
| Logo | Quill-pen hybrid icon with "B.D. Murphy" in serif text file: "Icon_Quill_BDMurphy_Black_WhtBG.jpg" |
| Banner | 1200x200px illustrated timeline file: "Reading_History_banner_1200x200.jpg" |
| Primary Color | Navy blue (#1B3A6B) |
| Accent Color | Warm gold (#C8A96E) |
| Background | Parchment (#F5F0E8) |
| Heading Font | Playfair Display / Georgia fallback |
| Body Font | Lato / Calibri fallback |
### 1.4 General Behavior
All books and blog content displays in reverse chronological order by first-published date unless a manual sort position is assigned. Series content sorts by volume number within the series. UI chrome follows the site language setting; content stays in the language it was authored in.
### 1.5 WordPress Coding Standards Compliance
All code MUST comply with [WordPress Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/):
- **PHP:** PSR-12 compatible with WordPress conventions
- **JavaScript:** ES5+ with JSHint validation
- **CSS:** WordPress CSS coding standards
- **HTML:** Semantic HTML5, no inline styles except where unavoidable
- **Text Domain:** `bd-murphy` for theme, `bd-bookshelfhelper` and `bd-simpleemail` for plugins
---
## 2. Theme - Global Layout
### 2.1 Header
- Banner image (1200x200px) via `add_theme_support('custom-header')`
- Default asset: `Reading_History_banner_1200x200.jpg`
- Site logo via `add_theme_support('custom-logo')`
- Default logo asset: 'Icon_Quill_BDMurphy_Black_WhtBG.jpg'
- Admin bar offset handled via `body.admin-bar` rules
### 2.2 Navigation Menu
- Registered via `register_nav_menus(['primary' => 'Primary Menu'])`
- Default items: Home, Blog, Books, Series (conditional), Reviews, About, Contact
- Sticky behavior with gold top border on scroll
- Series menu item conditionally rendered by BD BookHelper
### 2.3 Footer
- Three-zone layout: social icons, donate buttons, utility links
- Two widget areas: "Footer Left" and "Footer Right"
- Sign-up prompt with link to /signup/ page
### 2.4 WordPress Customizer Settings
| Setting | Function | Location |
|---------|----------|----------|
| Site title | `get_bloginfo('name')` | Settings > General |
| Logo | `the_custom_logo()` | Customizer > Site Identity |
| Banner | `get_header_image()` | Customizer > Header Image |
| Background | Custom background | Customizer > Background |
| Primary color | `get_theme_mod('primary_color', '#1B3A6B')` | Customizer > Colors |
| Accent color | `get_theme_mod('accent_color', '#C8A96E')` | Customizer > Colors |
| Fonts | `get_theme_mod('heading_font', ...)`, etc. | Customizer > Typography |
### 2.5 Theme Options Admin Screen
Settings saved via WordPress Settings API in `bdm_theme_options`:
- Social media URLs (X/Twitter, Facebook, LinkedIn, Instagram)
- Donation URLs (Buy Me a Coffee, PayPal)
- Author photo (media attachment ID)
- Sticky navigation toggle
- Posts per page
- Show read time toggle
- Business information (for CAN-SPAM compliance)
---
## 3. Theme - Pages
### 3.1 Home Page (front-page.php)
Top will be the banner using 100% of screen.
Line two will be the menu line. Each menu item as shown the menu setup.
Two-column layout: 66% content, 34% sidebar. Content via WordPress block editor. Sidebar contains book shelf widget with the default as custom HTML with the goodreads book list for bdmurphy.
The main page will have a short url from the plugin BookShelf Helper [all-books]
### 3.2 Blog Page (/blog/)
Template: `archive.php` or `home.php`. Lists posts with title, date, category, excerpt, featured image. Paginated (10 per page). Social sharing on individual posts.
### 3.3 All Books Page (/books/)
Template: `archive-book.php`. Stacked list with cover, title, author, ratings, format toggle, retailer buttons. Each book links to its detail page via short URL.
### 3.3.1 Shortcode Display
Books displayed via `[bd-book]` or `[bd-books]` shortcodes:
- Cover image (clickable, links to detail page)
- Title (clickable, links to detail page)
- Author/Pen name
- Blurb excerpt (HTML content, truncated)
- Retailer purchase buttons
- "View details" link to book detail page
### 3.4 Individual Book Page (single-book.php)
Full-width template with:
- Language selector tabs (EN, FR, DE, ES, RU, ZH-S, IT, PT, JA)
- Cover image (220x310px)
- Title, pen name, series info
- Ratings, format toggle, retailer links
- Read preview, pull quotes, reviews
- Share button (copies short URL)
- Social sharing buttons
**Short Link Access:** Books are accessible via short URLs using the slug defined in book metadata. URL pattern: `/{short-slug}` (e.g., `/ph/` for "Pandemic Hacker")
**Blurb HTML:** Descriptions are stored as HTML, allowing rich formatting (paragraphs, bold, italic, links) in book listings and detail pages.
### 3.4.1 My Books Submenu
The "My Books" menu item displays a dropdown submenu with links to each book's detail page:
```
My Books (top-level)
├── Book Title 1
├── Book Title 2
└── ...
```
- Each book appears as a submenu item
- Links point to the book's detail page using short URL
- Books sorted alphabetically by title
- Only published books shown
### 3.5 Reviews Page (/reviews/)
Template: `page-reviews.php`. Two-column grid with book cards and review summaries.
### 3.6 About Page (/about/)
Template: `page-about.php`. Two-column with author bio, awards, donate buttons, sign-up prompt.
### 3.7 Series Pages
- Listing: `archive-series.php` at /series/
- Individual: `taxonomy-series.php` or `single-series.php` at /series/{slug}/
- Series nav item hidden when no series exist
### 3.8 Sign-up Page (/signup/)
Template: `page-signup.php`. Email, first name, opt-in checkbox. Shortcode `[bdm_signup]` available.
### 3.9 Social Sharing Buttons
Platforms: Facebook, X/Twitter, LinkedIn, WhatsApp, Copy Link. Uses Web Share API where available. Book pages share the short URL.
### 3.10 Utility Pages
- Privacy Policy: standard WordPress page
- Unsubscribe: single email input + one-click HMAC tokens
- Contact: AJAX form with honeypot spam protection
---
## 4. Design System
### 4.1 Color Palette (CSS Custom Properties)
```css
:root {
--bdm-navy: #1B3A6B;
--bdm-slate: #2E6DA4;
--bdm-sky: #4A90D9;
--bdm-gold: #C8A96E;
--bdm-cream: #F5F0E8;
--bdm-parchment: #EDE4D0;
--bdm-body-text: #2C2C2C;
--bdm-mid-grey: #555555;
--bdm-light-grey: #888888;
--bdm-active-green: #2D6A4F;
}
```
### 4.2 Typography
- Headings: Playfair Display, Georgia, serif
- Body: Lato, Calibri, sans-serif
- Google Fonts loaded via `wp_enqueue_style()` with `display=swap`
### 4.3 Image Sizes
| Size | Dimensions | Use |
|------|-----------|-----|
| shelf-thumb | 44x62px (crop) | Book shelf widget |
| blog-thumb | 100x68px (crop) | Blog post thumbnails |
| rev-thumb | 52x74px (crop) | Review page covers |
| book-cover | 130x185px (crop) | All books page |
| book-page-cover | 220x310px (crop) | Individual book page |
| series-card | 400x280px (crop) | Series listing |
| series-header | 1200x300px (crop) | Series header |
### 4.4 Responsive Breakpoints
| Breakpoint | Layout |
|------------|--------|
| >=1200px | Full two-column, desktop nav |
| 768-1199px | Single column, hamburger menu |
| <768px | Single column, touch-friendly targets (44x44px min) |
---
## 5. Plugin: BD-BookShelfHelper
### 5.1 Overview
Manages all book-related data, series information, and pen name configuration. Registers "book" custom post type and "series" taxonomy.
### 5.2 Book Data Entry
**5.2.1 Data Entry Interface**
The plugin provides a dedicated book data entry page with the following UI requirements:
- **Input Fields:** Each data field has a dedicated input control (text, textarea, select, date picker as appropriate)
- **Labels:** Every field has a visible label using `<label>` elements properly associated via `for` attribute
- **Hints:** Helper text below each field provides context, format requirements, and examples
- **Validation:**
- Real-time validation on blur with visual feedback (green checkmark for valid, red border + message for invalid)
- Server-side validation on save with error messages displayed above problematic fields
- Required fields marked with asterisk and validated before submission
- Format validation: ISBN-13 (13 digits), ASIN (10 alphanumeric), dates (YYYY-MM-DD), URLs (valid format)
- **Organization:** Fields grouped into logical sections with collapsible headers:
- Basic Information (title, pen name, publication date)
- Identifiers (ISBN-13, ASIN)
- Series & Order (series selection, volume number)
- Content (blurb, preview excerpt, pull quotes)
- Metadata (genre tags, Amazon search terms)
- Retailer Links (Amazon, Apple, Google, B&N, Kobo)
- Reviews (add/edit reviews)
- **Multi-language Tabs:** Language selector tabs for entering content in each supported language
- **Auto-save Draft:** Automatic saving of work in progress every 60 seconds
- **Preview:** Button to preview how the book will appear on the front-end
**Core Fields (language-independent):**
- Cover image (per language)
- Pen name (from configured list)
- Publication date, sort order
- ISBN-13, ASIN
- Series, volume number
- Short link slug
**Content Fields (per language):**
- Title, blurb, preview excerpt
- Pull quotes (up to 5)
- Genre tags (min 3)
- Amazon search terms
**Reviews (per book):**
- Comment text, source name, source URL
- AI-generated 3-line synopsis
- Display order
**Supported Languages:** EN, FR, ES, DE, IT, PT, RU, JA, ZH (Simplified)
### 5.3 Series Management
- Series name, slug, cover graphic, blurb, display order
- Book series assignment with volume number
- Conditional nav item rendering
### 5.4 Pen Name Management
- Up to 10 pen names
- Active/inactive toggle
- Notes field (internal only)
- Per-book assignment
### 5.5 Retailer Purchase Links
| Retailer | Formats | URL Pattern |
|----------|---------|-------------|
| Amazon | Ebook, Paperback, Hardback | `amazon.com/dp/{ASIN}?tag={affiliate_id}` |
| Apple Books | Ebook | URL with `at=` parameter |
| Google Play | Ebook | URL with partner code |
| Barnes & Noble | All | URL with affiliate parameter |
| Kobo | Ebook | URL with affiliate parameter |
### 5.6 Short Link System
**Book Short Links:**
- Auto-generated from first letter of significant words
- Examples: "Pandemic Hacker" → `/ph/`, "Deal Hunter" → `/dh/`
- Conflict check with incrementing numbers
**Review Short Links:**
- Pattern: `{book-slug}r{site-code}` (e.g., `dhrG` = Deal Hunter Goodreads)
- Lifecycle: Reserved → Pending → Active
### 5.7 Goodreads Integration
- Daily WP-Cron refresh
- Parses widget HTML for book data
- Author ID: 30469651
### 5.8 Amazon Ratings
- 6-hour WP-Cron scrape
- Staleness indicator on failure
- **Warning:** May conflict with Amazon Terms of Service
### 5.9 SEO Generation
Auto-generated on book save:
- Title tag, meta description
- Open Graph, Twitter Card tags
- schema.org Book structured data
### 5.10 Admin Menu Structure
```
BD BookHelper (top-level menu)
├── All Books
├── Add Book
├── Data Entry (Enhanced interface per 5.2.1)
├── Series (taxonomy submenu)
├── Pen Names
├── Settings
│ ├── Affiliate Codes
│ ├── Goodreads
│ └── Amazon Scrape
└── Migrate (Import from BD Books plugin)
```
### 5.11 Shortcodes
The plugin provides WordPress shortcodes for displaying book content:
| Shortcode | Description |
|-----------|-------------|
| `[bd-book book="1"]` | Display single book by ID or title |
| `[bd-books]` | Display all books in a list |
**Usage:**
```php
// Single book by ID
[bd-book book="42"]
// Single book by title
[bd-book book="Pandemic Hacker"]
// All books
[bd-books]
// Limit number of books
[bd-books limit="5"]
```
### 5.12 Migration from BD Books Plugin
The plugin includes a migration tool to import books from the original "BD Books" (bd-book-listing) plugin:
**Migration Process:**
1. Reads from `{$wpdb->prefix}bd_books` table
2. Creates new book CPT entries
3. Maps author to pen names (creates new if needed)
4. Imports descriptions as blurbs (English)
5. Imports cover images (WordPress media or external URLs)
6. Parses and maps purchase links to retailer fields
**Migrated Fields:**
| BD Books Field | BookHelper Target |
|---------------|------------------|
| title | Post title |
| author | Pen name (lookup or create) |
| description | blurb_{lang} meta |
| cover_image_id | Featured image (WordPress media) |
| cover_image_url | Downloaded and imported as attachment |
| purchase_links | Retailer URL metas (Amazon, Apple, B&N, Kobo, Google) |
**Admin Access:** BD BookHelper → Migrate
---
## 6. Plugin: BD-SimpleEmail
### 6.1 Overview
Handles email newsletter creation, subscriber management, and delivery via Gmail SMTP. AI-assisted composition via Gemini API.
### 6.2 Configuration
| Setting | Storage |
|---------|---------|
| Gmail address | `wp_options` |
| App password | Encrypted (openssl_encrypt) |
| Sender name | `wp_options` |
| Reply-to | `wp_options` |
| Gemini API key | Encrypted `wp_options` |
### 6.3 Email Composition Workflow
1. User opens "New Newsletter"
2. Enters subject and AI prompt (optional)
3. Plugin generates HTML via Gemini API
4. User edits draft
5. Optional: publish as blog post
6. Click "Send Newsletter" → sends to all subscribers via Gmail SMTP
### 6.4 Email Template
**Header:** Site name/logo
**Body:** Composed content with affiliate links
**Footer (mandatory):**
- Website URL
- One-click unsubscribe (HMAC-signed token)
- Full business address (CAN-SPAM)
- Unsubscribe text link
### 6.5 Subscriber Management
**Schema:**
```sql
bd_subscribers (
id PRIMARY KEY AUTO_INCREMENT,
email UNIQUE NOT NULL,
first_name VARCHAR(100),
status ENUM('subscribed', 'unsubscribed'),
subscribed_at DATETIME,
unsubscribed_at DATETIME NULL,
ip_address VARCHAR(45),
UNSUBSCRIBE_TOKEN VARCHAR(64) -- pre-generated
)
```
**One-Click Unsubscribe:**
- HMAC-signed token in every email
- Token format: `hash_hmac('sha256', email, AUTH_KEY)`
- Satisfies Gmail/Yahoo delivery requirements
### 6.6 Admin Menu Structure
```
BD SimpleEmail (top-level menu)
├── Subscribers
├── New Newsletter
└── Settings
└── Gmail Configuration
└── Gemini API
└── About
```
---
## 7. Database Schema
### 7.1 BD BookHelper Tables
```sql
-- Core book record
CREATE TABLE {$wpdb->prefix}bd_books (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
post_id BIGINT NOT NULL,
pen_name_id BIGINT,
series_id BIGINT,
volume_number INT,
asin VARCHAR(20),
isbn VARCHAR(20),
publish_date DATE,
sort_order INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY post_id (post_id)
);
-- Per-language content
CREATE TABLE {$wpdb->prefix}bd_book_meta (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
book_id BIGINT NOT NULL,
language_code VARCHAR(5) NOT NULL,
title VARCHAR(255),
blurb TEXT,
preview_excerpt TEXT,
pull_quotes JSON,
UNIQUE KEY book_language (book_id, language_code)
);
-- Series definitions
CREATE TABLE {$wpdb->prefix}bd_series (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE,
cover_attachment_id BIGINT,
header_attachment_id BIGINT,
blurb TEXT,
display_order INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Pen names
CREATE TABLE {$wpdb->prefix}bd_pen_names (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
pen_name VARCHAR(255) NOT NULL,
notes TEXT,
active TINYINT(1) DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Reviews
CREATE TABLE {$wpdb->prefix}bd_reviews (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
book_id BIGINT NOT NULL,
comment_text TEXT,
synopsis TEXT,
source_name VARCHAR(255) NOT NULL,
source_url VARCHAR(500),
display_order INT DEFAULT 0,
KEY book_id (book_id)
);
-- Ratings cache
CREATE TABLE {$wpdb->prefix}bd_ratings (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
book_id BIGINT NOT NULL,
source VARCHAR(20) NOT NULL,
rating DECIMAL(3,2),
review_count INT,
last_updated DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY book_source (book_id, source)
);
-- Review short links
CREATE TABLE {$wpdb->prefix}bd_review_shortlinks (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
book_id BIGINT NOT NULL,
site_code VARCHAR(5) NOT NULL,
slug VARCHAR(20) NOT NULL UNIQUE,
destination_url VARCHAR(500),
status VARCHAR(20) DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
activated_at DATETIME
);
```
### 7.2 BD SimpleEmail Tables
```sql
-- Subscribers
CREATE TABLE {$wpdb->prefix}bd_subscribers (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
first_name VARCHAR(100),
status VARCHAR(20) DEFAULT 'subscribed',
subscribed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
unsubscribed_at DATETIME,
ip_address VARCHAR(45),
unsubscribe_token VARCHAR(64)
);
-- Newsletter log
CREATE TABLE {$wpdb->prefix}bd_newsletters (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
subject VARCHAR(255) NOT NULL,
body_html TEXT,
sent_at DATETIME DEFAULT CURRENT_TIMESTAMP,
recipient_count INT DEFAULT 0
);
```
---
## 8. Design Decisions
| Decision | Answer | Rationale |
|----------|--------|----------|
| Child theme support | No | Customization via Customizer and Theme Options only |
| Dark mode | No | Light mode only, parchment palette |
| Sign-up form | Dedicated page (/signup/) | Shortcode `[bdm_signup]` also available |
| Visitor-submitted reviews | No | Admin-entered only |
| Social sharing | Yes | Web Share API with platform fallback |
| Series pages | Yes | Full series system with conditional nav |
| Affiliate codes | Optional | System works without, adds when available |
| One-click unsubscribe | Yes | HMAC-signed token per subscriber |
---
## 9. Implementation Standards
### 9.1 WordPress Architecture
1. **Custom Post Types:** "book" CPT registered by BD BookHelper (theme queries, doesn't register)
2. **Taxonomies:** "series" registered by BD BookHelper, attached to book CPT
3. **Rewrite Rules:** Short links via `add_rewrite_rule()`
4. **Internationalization:** Text domains (`bd-murphy`, `bd-bookshelfhelper`, `bd-simpleemail`), `load_theme_textdomain()` in functions.php
5. **Scheduled Tasks:** WP-Cron for Goodreads (daily), Amazon ratings (6 hours), review links (daily)
6. **Asset Loading:** All CSS/JS via `wp_enqueue_style()` and `wp_enqueue_script()` with handles, dependencies, version strings
### 9.2 Security Requirements
| Requirement | Implementation |
|-------------|----------------|
| Secrets storage | `openssl_encrypt()` with AES-256-CBC, key from AUTH_KEY |
| Unsubscribe tokens | `hash_hmac('sha256')` with AUTH_KEY |
| Output escaping | `esc_html()`, `esc_url()`, `esc_attr()`, `wp_kses_post()` |
| Input sanitization | `sanitize_text_field()`, `sanitize_email()`, `absint()`, etc. |
| CSRF protection | `wp_verify_nonce()` on all form submissions |
| Nonce verification | Nonce field + verification on every AJAX and form POST |
| Affiliate codes | Server-side application only, never in client-side code |
| Spam protection | Contact form honeypot field |
### 9.3 Required Theme Support Declarations
```php
add_theme_support('title-tag');
add_theme_support('custom-logo', array(
'width' => 200,
'height' => 200,
'flex-width' => true,
'flex-height' => true,
));
add_theme_support('custom-header', array(
'default-image' => BDM_URI . '/assets/banner_1200x200_v3.png',
'width' => 1200,
'height' => 200,
'flex-height' => false,
));
add_theme_support('custom-background', array(
'default-color' => 'F5F0E8',
));
add_theme_support('post-thumbnails');
add_theme_support('html5', array(
'search-form', 'comment-form', 'comment-list',
'gallery', 'caption', 'script', 'style'
));
add_theme_support('automatic-feed-links');
add_theme_support('wp-block-styles');
add_editor_style('assets/editor-style.css');
add_image_size('shelf-thumb', 44, 62, true);
add_image_size('blog-thumb', 100, 68, true);
add_image_size('rev-thumb', 52, 74, true);
add_image_size('book-cover', 130, 185, true);
add_image_size('book-page-cover', 220, 310, true);
add_image_size('series-card', 400, 280, true);
add_image_size('series-header', 1200, 300, true);
```
### 9.4 Template File Structure
```
bd-murphy-author/
├── style.css (Theme header: Name, Author, Version, Text Domain)
├── functions.php (add_theme_support, register_nav_menus, etc.)
├── header.php (wp_head, banner, nav, body_class())
├── footer.php (Footer zones, wp_footer)
├── front-page.php (Home page)
├── archive.php (Blog posts)
├── archive-book.php (All books)
├── single-book.php (Individual book)
├── archive-series.php (Series listing)
├── taxonomy-series.php (Individual series)
├── page.php (Default page)
├── page-full-width.php (Template: Full Width)
├── page-no-sidebar.php (Template: No Sidebar)
├── page-about.php (Template: About)
├── page-reviews.php (Template: Reviews)
├── page-signup.php (Template: Sign Up)
├── page-unsubscribe.php (Template: Unsubscribe)
├── page-contact.php (Contact with AJAX)
├── 404.php
├── search.php
└── inc/
├── customizer.php
├── theme-options.php
├── template-tags.php
├── widgets.php
└── nav-menu.php
```
### 9.5 Required Template Tag Usage
| Tag | Location | Purpose |
|-----|----------|---------|
| `body_class()` | `<body>` | Plugin hooks, admin bar offset |
| `post_class()` | Article wrappers | Plugin hooks, state classes |
| `wp_head()` | Before `</head>` | Plugin hooks, styles |
| `wp_footer()` | Before `</body>` | Plugin hooks, scripts |
| `the_custom_logo()` | Header | Logo output |
| `get_header_image()` | Header | Banner output |
| `wp_nav_menu()` | Header, Footer | Navigation menus |
| `dynamic_sidebar()` | Sidebar areas | Widget areas |
| `do_action('bdm_post_published', $post_id)` | On publish | Future plugin hook |
### 9.6 Hooks Reference
**Actions:**
- `bdm_post_published` — Fired on book/blog post publish (future plugin hook)
**Filters:**
- `bdm_nav_menu_items` — Filter for Series menu conditional
- `bdm_social_share_url` — Filter share URLs
- `bdm_email_footer_content` — Filter email footer
---
## 10. Quality Requirements
### 10.1 Testing Standards
**Unit Tests (Required):**
- All helper functions must have PHPUnit tests
- Minimum 80% code coverage for utility functions
- Test data factories for books, series, subscribers
**Integration Tests:**
- Plugin activation/deactivation
- Database table creation
- Custom post type registration
- REST API endpoints
**E2E Tests (Recommended):**
- Sign-up flow
- Unsubscribe flow
- Newsletter send flow
- Contact form submission
### 10.2 Accessibility Requirements (WCAG 2.1 AA)
| Requirement | Implementation |
|-------------|----------------|
| Keyboard navigation | All interactive elements focusable |
| Skip links | "Skip to content" link in header |
| ARIA labels | `aria-label`, `aria-expanded`, `role` attributes |
| Color contrast | Minimum 4.5:1 for text |
| Focus indicators | Visible focus styles |
| Screen reader | Semantic HTML, screen reader text |
| Touch targets | Minimum 44x44px on mobile |
### 10.3 Performance Targets
| Metric | Target |
|--------|--------|
| Page load | < 2 seconds |
| Time to first byte | < 600ms |
| Lighthouse Performance | > 90 |
| Lighthouse Accessibility | > 95 |
**Optimization Requirements:**
- Lazy loading for images (`loading="lazy"`)
- Minified CSS and JavaScript in production
- GZIP compression enabled
- Object caching enabled (Redis/Memcached recommended)
- Database query optimization with proper indexing
### 10.4 Security Audit Checklist
- [ ] All user input sanitized
- [ ] All output escaped
- [ ] Nonces verified on all form submissions
- [ ] Capabilities checked on admin actions
- [ ] SQL injection prevention (prepared statements)
- [ ] XSS prevention (escaping)
- [ ] CSRF protection (nonces)
- [ ] Stored secrets encrypted
- [ ] File upload validation
- [ ] Rate limiting on contact form
- [ ] Security headers (X-Content-Type-Options, X-Frame-Options, CSP)
---
## 11. Deployment Checklist
### 11.1 Pre-Deployment
- [ ] All code passes WordPress Coding Standards sniffs
- [ ] Unit tests pass with >80% coverage
- [ ] No debug output in production code
- [ ] Plugin uninstall.php created
- [ ] Plugin README.txt with installation instructions
- [ ] Theme screenshot.png (1200x900px)
### 11.2 Server Requirements
- [ ] WordPress 6.0+ installed
- [ ] PHP 7.4+ (8.2 recommended)
- [ ] MySQL 5.7+ or MariaDB 10.3+
- [ ] HTTPS enabled with valid certificate
- [ ] Memory limit >= 256M
- [ ] Max execution time >= 60s
- [ ] WP-Cron configured (DISABLE_WP_CRON = false)
### 11.3 Configuration
- [ ] AUTH_KEY, SECURE_AUTH_KEY, LOGGED_IN_KEY, NONCE_KEY defined
- [ ] `WP_DEBUG` disabled in production
- [ ] `WP_DEBUG_LOG` disabled in production
- [ ] `SAVEQUERIES` disabled in production
- [ ] Object cache (Redis/Memcached) configured
- [ ] CDN configured for static assets (optional)
### 11.4 Plugin Installation Order
1. Install and activate **BD-BookShelfHelper** first
2. Install and activate **BD-SimpleEmail** second
3. Upload and activate **BDMurphy Bookshelf** theme
4. Configure Theme Options (Appearance > Theme Options)
5. Configure BD BookHelper Settings
6. Configure BD SimpleEmail Settings
### 11.5 Post-Installation
- [ ] Permalinks flushed (Settings > Permalinks > Save)
- [ ] Sample data installed (if desired)
- [ ] Required pages created (/signup, /unsubscribe, /contact)
- [ ] Navigation menus configured
- [ ] Book shelf widget placed in Home Sidebar
- [ ] Test sign-up flow
- [ ] Test newsletter send
- [ ] Verify social sharing works
- [ ] Test mobile responsiveness
- [ ] Verify accessibility with screen reader
### 11.6 Ongoing Maintenance
- [ ] WordPress core updates tested before production deployment
- [ ] Plugin updates tested before production deployment
- [ ] Theme updates tested before production deployment
- [ ] Database backups before any update
- [ ] Security audit quarterly
- [ ] Performance audit bi-annually
---
## Appendix A: Disallowed Functions & Patterns
The following are NOT permitted in this project:
| Category | Disallowed |
|----------|------------|
| Database | `mysql_*` functions (use `$wpdb`) |
| Filesystem | `file_get_contents()` with URLs, direct file inclusion |
| Security | `eval()`, `exec()`, `shell_exec()`, `system()` |
| Output | `echo` without escaping, `print` without escaping |
| Deprecated | Any function marked deprecated in WordPress 6.0+ |
## Appendix B: Recommended Tools
- **Local Development:** Local by Flywheel, Docker, or VVV
- **Code Quality:** PHPStan, ESLint, Prettier
- **Testing:** PHPUnit, WP_Mock, Codeception
- **Security:** WPScan, Sucuri, Wordfence
- **Performance:** Query Monitor, New Relic, GTmetrix
## Appendix C: Sample Data
On plugin/theme activation, install sample data:
1. Pen name "B.D. Murphy" as active default
2. Book "Pandemic Hacker" with placeholder cover
3. Pre-reserved review short links: phrA, phrG, phrB, phrK, phrP, phrL
4. Pre-configured retailer links for sample book
5. Three sample blog posts
6. Goodreads Author ID: 30469651
"Clear sample data" button in Theme Options removes all sample content.
---
**End of Specification v2.0**
For page mockups, see: BD-Murphy-Page-Mockups-v1.2.docx

Binary file not shown.

View File

@@ -0,0 +1,467 @@
BDMurphy BookShelf
WordPress System
Technical Specification
Version 2.0 ? May 2026
Theme: BDMurphy-Bookshelf <20> Plugin: BDMurphy-BookShelf <20> Plugin: BD-BookShelfHelper
1. Project Overview
1.1 Purpose
This specification defines a custom WordPress theme and two companion plugins for author B.D. Murphy's website. The system allows the author to showcase books, publish blog posts, manage reviews, and send newsletters with minimal technical knowledge required after initial setup. The theme is built from scratch with no parent framework and follows WordPress coding standards throughout.
The core design principle is enter once, display everywhere: the author enters book details once in the BD-BookShelfHelper plugin and the system generates all book pages, series pages, and review displays automatically. No individual page creation or editing is required for book content.
1.2 Deliverables
Theme: BDMurphy-Bookshelf Custom WordPress theme. Fully Customizer-driven. Follows WordPress standards and best practices. Provides the visual presentation layer for all pages. Ships with default logo, banner, and sample content.
Plugin: BDMurphy-BookShelf Manages the home page book shelf sidebar widget, subscriber sign-up, subscriber management, and email newsletter creation and delivery. Includes AI-assisted newsletter composition via the Gemini API.
Plugin: BD-BookShelfHelper The book data management plugin. Stores all book information (blurbs, genres, reviews, cover images), series, and pen names. Generates book pages, review pages, and series pages from entered data. Manages short links, review links, Goodreads integration, Amazon ratings, and SEO. Full specification: BD-BookShelfHelper-Plugin-Specification-v1_0.docx.
Each deliverable package includes: a description, a setup and customization guide, and a display visual for the WordPress plugin or theme directory listing. Each deliverable is a ready-to-upload package installable via the WordPress admin. Deliverables are built one at a time and tested individually before integration testing.
1.3 Design Identity
<20> Logo: Quill-pen hybrid icon with "B.D. Murphy" in serif text <20> provided asset ships as default (BD_BookShelf_Logo.png).
<20> Banner: 1200<30>250px illustrated timeline (scrolls from quill to modern ebooks) <20> ships as default (Reading_through_time.png).
<20> Palette: Navy blue (#1B3A6B) primary, warm gold (#C8A96E) accents, parchment (#F5F0E8) background.
<20> Typography: Playfair Display / Georgia fallback for headings; Lato / Calibri fallback for body.
<20> Mood: Cozy, casual reading <20> literary but not stiff. Light mode only; no dark mode.
1.4 General Behavior
<20> All content displays in reverse chronological order by first-published date unless a manual sort position is assigned.
<20> Series content sorts by volume number within the series.
<20> UI chrome (menus, labels, system text) follows the site language setting; content stays in the language it was authored in.
<20> All purchase links support affiliate code injection, applied server-side using per-retailer URL patterns.
<20> All short links share one conflict-check pool <20> no slug can clash with any other WordPress slug.
<20> The site represents one author who may publish under multiple pen names. The site header and bio always reflect the author identity; book pages show the pen name the book was published under.
2. Theme <20> Global Layout
2.1 Header
<20> The author's logo, sized to 250<35>250px, appears at the top left of every page. BD_BookShelf_Logo.png ships as the default.
<20> The banner image, 250px high, appears to the right of the logo and is centered on the page. Reading_through_time.png ships as the default.
<20> The logo and banner are the first elements on every page, above the navigation.
<20> Both are implemented as WordPress configurable items. The author replaces them via Appearance > Customize > Header Image.
<20> The default site title is the website URL. The default tagline (blogdescription) is "Expand your mind."
<20> Site title and tagline display as text fallback when no banner image is set.
<20> Nothing appears above the banner except the browser chrome and the WordPress admin bar (32px when logged in). Admin bar offset handled via body.admin-bar rules.
Note: Banner height is locked at 250px maximum in the custom-header declaration.
2.2 Navigation Menu
Registered via register_nav_menus(['primary' => 'Primary Menu']). Assigned in Appearance > Menus.
Sits directly below the banner with zero gap. Spans 100% viewport width. Background: navy (#1B3A6B). Active item: white text with gold 2px underline.
Default Menu Items
Home <20> Blog <20> Books <20> Reviews <20> Series <20> About <20> Register
Each item has a pre-built default page installed at theme activation with simple example content.
Home General news and blog posts. Two-column layout (see Section 3.1).
Blog WordPress posts listed in date order, most recent first. Posts tagged as blog posts (not reviews).
Books All books page. Ships with "Pandemic Hacker" as sample content.
Reviews Without BD-BookShelfHelper: WordPress posts tagged "reviews". With BD-BookShelfHelper: generated review display page showing review information in multiple languages with pull-quote callouts.
Series Without BD-BookShelfHelper: standard WordPress page with two sample book entries. With BD-BookShelfHelper: generated series listing from plugin data. The Series nav item is hidden when no series records exist.
About Standard WordPress page with author biography and details (see Section 3.6).
Register Sign-up page for newsletter and announcement subscriptions (see Section 3.8). Managed by the BDMurphy-BookShelf plugin.
When BD-BookShelfHelper is installed, a language selector dropdown appears right-justified at the far right of the navigation bar. The default language is configured in settings (default: EN).
When BDMurphy-BookShelf plugin is installed, the Register page provides selection for opt-in / opt-out of categories: Newsletter, New Book Announcements, and Events. Registered users can return to this page to update their preferences.
Sticky Behavior
<20> Default: sticky <20> CSS position: sticky; top: 0; z-index: 100. Pins to viewport top as user scrolls past the banner.
<20> When sticky and the banner has scrolled out of view, a 2px gold top border appears on the nav bar.
<20> body.admin-bar .site-nav { top: 32px } applied when the WordPress admin bar is visible.
<20> All in-page anchor targets use scroll-margin-top: 48px to avoid content being hidden behind the sticky nav.
<20> A Theme Options toggle (sticky_nav, default: true) allows disabling sticky behavior.
Note: Sticky nav requires the nav parent element to have defined height, not overflow: hidden.
2.3 Footer
<20> Three-zone layout: social icons (left), donate buttons (centre), utility links (right).
<20> Social links: X/Twitter, Facebook, LinkedIn, Instagram, BlueSky. URLs configured in Appearance > Theme Options > Social Media.
<20> "Buy Me a Coffee" and PayPal donate URLs configured in Appearance > Theme Options > Donate Links.
<20> Utility links: Contact | About | Privacy Policy | Unsubscribe <20> registered as footer menu via register_nav_menus(['footer']).
<20> Footer also includes a brief sign-up prompt with a link to the /signup/ page.
<20> Two registered widget areas: "Footer Left" and "Footer Right" <20> available in Appearance > Widgets.
2.4 WordPress Customizer Settings
All visual settings are exposed through the WordPress Customizer (Appearance > Customize) using standard WordPress APIs.
Setting WordPress mechanism / function Admin location
Site title get_bloginfo('name') / Settings > General Customizer > Site Identity
Tagline get_bloginfo('description') Customizer > Site Identity
Logo upload add_theme_support('custom-logo') / the_custom_logo() Customizer > Site Identity > Logo
Banner image add_theme_support('custom-header') / get_header_image() Customizer > Header Image
Page background add_theme_support('custom-background') / default #F5F0E8 Customizer > Background
Primary color get_theme_mod('primary_color', '#1B3A6B') Customizer > Colors > Primary
Accent color get_theme_mod('accent_color', '#C8A96E') Customizer > Colors > Accent
Heading font get_theme_mod('heading_font', 'Playfair Display') Customizer > Typography > Headings
Body font get_theme_mod('body_font', 'Lato') Customizer > Typography > Body
Primary navigation register_nav_menus(['primary']) Customizer > Menus
Footer navigation register_nav_menus(['footer']) Customizer > Menus
Footer widgets register_sidebar() x 2 Customizer > Widgets > Footer
Note: All get_theme_mod() calls include a default value so the theme displays correctly immediately after activation.
Note: Colors output as CSS custom properties (--bdm-navy, --bdm-gold, --bdm-cream, etc.) via the wp_head hook.
2.5 Theme Options Admin Screen
Settings that do not suit live Customizer preview are in Appearance > Theme Options, saved via the WordPress Settings API.
Social: X/Twitter URL URL for X/Twitter profile <20> displayed as icon in footer.
Social: Facebook URL URL for Facebook page.
Social: LinkedIn URL URL for LinkedIn profile.
Social: Instagram URL URL for Instagram profile.
Social: BlueSky URL URL for BlueSky profile.
Donate: Buy Me a Coffee URL Full URL to buymeacoffee.com page.
Donate: PayPal URL Full PayPal.me or donation URL.
Donate: Venmo URL Full Venmo donation URL.
Author photo WordPress media attachment ID <20> defaults to built-in smiley SVG when empty.
Sticky navigation Boolean toggle, default: ON.
Posts per page (blog) Integer, default: 10.
Language default selection Dropdown: EN, FR, DE, ES. Shown in the language selector when BD-BookShelfHelper is active.
Show read time on posts Boolean, default: ON. Displays estimated read time on blog posts.
Business name Legal name <20> appears in email footer for CAN-SPAM compliance.
Business address line 1 Street address <20> required for CAN-SPAM.
Business address line 2 Suite, unit, PO Box <20> optional.
City / State / Postal code Required for CAN-SPAM.
Country Required for CAN-SPAM.
Phone number Optional <20> appears in email footer if provided.
Note: Business information fields are required by CAN-SPAM law for commercial email. The BDMurphy-BookShelf plugin pulls these values into every outgoing email footer automatically.
3. Theme <20> Pages
3.1 Home Page
Template: front-page.php. Set as static front page via Settings > Reading.
Two-column layout: left 66%, right 34%.
Left Column <20> News & Blog
<20> Content managed via the WordPress block editor on a WordPress Page. No custom meta fields.
<20> Editorial, hand-curated. Author news, announcements, featured blog post headings.
Right Column <20> Book Shelf Widget
<20> Without the BDMurphy-BookShelf plugin installed: a placeholder widget displays "BDMurphy-BookShelf Plugin recommended."
<20> With BDMurphy-BookShelf plugin: custom WordPress widget registered by the plugin, placed in the "Home Sidebar" widget area.
<20> Each entry: cover thumbnail (44<34>62px), Amazon and Goodreads star ratings, retailer icon row.
<20> Clicking any cover navigates to that book's individual detail page. If BD-BookShelfHelper is not installed, the link goes to the /books/ page.
<20> BD-BookShelfHelper creates the detail page for each book when book data is entered. It displays details based on the reader's selected language.
<20> Sorted by publish date newest first unless manual order is set.
3.2 Blog Page (/blog/)
Template: archive.php or home.php. Set as posts page in Settings > Reading.
<20> Lists all published blog posts, newest first: title (linked), date, category tag, excerpt (~150 words), featured image thumbnail (100<30>68px).
<20> Paginated <20> 10 posts per page, configurable via Theme Options.
<20> Social sharing buttons at the bottom of each individual blog post (see Section 3.9).
3.3 All Books Page (/books/)
Template: archive-book.php. Stacked list, most recent first. If series exist, a "Browse by series" link appears at the top pointing to /series/.
<20> Each entry: cover image (130<33>185px, left), author pen name, title, ratings, format toggle, retailer buttons, blurb, and review excerpt.
<20> Format toggle (Ebook / Paperback / Hardback) filters retailer buttons. Ebook-only retailers (Apple Books, Google Play) disabled when Paperback or Hardback is selected.
<20> All retailer links include affiliate codes applied server-side.
<20> BD-BookShelfHelper provides an "Update Links" button to scan retailer websites and verify purchase link URLs.
<20> If a book belongs to a series, a "Part of [Series Name]" label links to /series/{slug}/.
3.4 Individual Book Page
Template: single-book.php. Canonical URL is the book's short slug (e.g. /ph/). Full-width template <20> no sidebar.
Page Content (Top to Bottom)
<20> Language selector tab bar (EN FR DE ES RU ZH-S) <20> switches title, blurb, and pull quotes to the selected language version.
<20> Cover image (220<32>310px), prominent, left-aligned.
<20> Book title and pen name (the name the book was published under).
<20> If the book belongs to a series: "Part of the [Series Name] series <20> Book [volume number]" displayed below the title. The series name links to /series/{slug}/. Styled similarly to Amazon's series display.
<20> Amazon and Goodreads star ratings.
<20> Format toggle (Ebook / Paperback / Hardback) <20> dark green active state.
<20> Retailer purchase buttons with affiliate codes.
<20> "Read preview" section and one-liner pull quotes.
<20> Review cards (comment text, source name, optional source link).
<20> Back-cover blurb.
<20> Share button <20> copies the book's short URL to clipboard with a toast confirmation. The short URL (e.g. authorbdmurphy.com/ph) is shared, not the full canonical URL.
<20> Social sharing buttons at bottom of page (see Section 3.9).
3.5 Reviews Page (/reviews/)
Template: page-reviews.php. Two-column card grid, sorted by book publication date newest first.
<20> Each card: book cover thumbnail (52<35>74px), title, review count, up to 3 review entries.
<20> Per review entry: source name (uppercase blue) + 3-line AI-generated synopsis (italic).
<20> Clicking a card or "View all reviews" opens the per-book review detail page.
<20> Review detail page: cached WordPress page with table of contents (anchor links) and full review text. Anchor targets use scroll-margin-top: 48px for sticky nav clearance.
<20> Generated or regenerated when a review is added in BD-BookShelfHelper or the author triggers regeneration.
3.6 About Page (/about/)
Template: page-about.php. Two-column layout.
Left Column
<20> Author real name and pen name(s) used <20> listing pen names is optional.
<20> Full biography text <20> entered via the WordPress block editor. One bio for the author (not per pen name).
<20> Awards list with gold dot markers.
<20> "Buy Me a Coffee" button and PayPal donate button <20> URLs from Theme Options.
<20> Sign-up prompt: a short "Stay updated" section with a link to the /signup/ page.
Right Column Sidebar
<20> Author photo: defaults to built-in smiley face SVG. Replaced via Appearance > Theme Options > Author Photo.
<20> Sign-up card: brief description + "Sign up for updates" button linking to /signup/.
Note: The About page bio is for the author as a person. Pen names listed here are informational only. Each book page shows the pen name the book was published under.
3.7 Series Pages
Series pages appear only when series are configured in BD-BookShelfHelper. The Series nav item is hidden when no series records exist.
Series Listing Page (/series/)
<20> Template: archive-series.php.
<20> Each series shown as a card: series cover graphic, series name (heading), series blurb/description, book count.
<20> Cards sorted by series creation date (most recent first) or manual order set in BD-BookShelfHelper.
<20> Clicking a series card navigates to /series/{slug}/.
Individual Series Page (/series/{slug}/)
<20> Template: taxonomy-series.php.
<20> Header: series cover graphic, series name, series blurb.
<20> Below: all books in the series displayed in volume number order (Book 1, Book 2, etc.).
<20> Each book entry: cover thumbnail, title, pen name, volume number label ("Book 1"), brief blurb excerpt, purchase link buttons.
<20> Works correctly when only one book exists in the series.
<20> Breadcrumb: Home > Series > [Series Name].
3.8 Registration <20> Sign-Up Page (/signup/)
Template: page-signup.php. Full-width, no sidebar. Managed by the BDMurphy-BookShelf plugin.
<20> Accessed from: About page sign-up prompt, About page sidebar card, footer sign-up link, and any "Subscribe" calls to action on the site.
<20> Fields: Email address (required), First name (optional), opt-in confirmation checkbox.
<20> Opt-in categories: Newsletter, New Book Announcements, Events.
<20> On submission: stores record in the BDMurphy-BookShelf plugin subscriber table with status = "subscribed". Displays a confirmation message.
<20> The page includes a brief description of what subscribers receive.
<20> Shortcode [bookshelf_signup] available for placing the form in other pages or widget areas.
<20> Registered users can return to this page to update their category preferences.
3.9 Social Sharing Buttons
Social sharing buttons appear at the bottom of individual blog posts and individual book pages only. Not shown on list pages (home, /books/, /blog/, /reviews/, /series/).
Platforms Facebook, X/Twitter, LinkedIn, WhatsApp, and a "Copy link" button. Additional platforms can be toggled on/off in Appearance > Theme Options > Social Sharing.
URL on book pages The book's short URL (e.g. authorbdmurphy.com/ph) <20> not the full canonical URL. Retrieved from the book's stored short slug.
URL on blog posts The standard WordPress post permalink.
Implementation Uses the Web Share API where available (modern mobile browsers). Falls back to platform-specific share URLs on desktop. No third-party sharing plugin dependency.
Share counts Not displayed <20> avoids third-party API calls and privacy concerns.
3.10 Utility Pages
Privacy policy (/privacy-policy/) Standard static WordPress Page. Template: page.php or page-no-sidebar.php.
Unsubscribe (/unsubscribe/) Single email input. Sets subscriber status to "unsubscribed" in the BDMurphy-BookShelf plugin. Accepts HMAC-signed one-click unsubscribe tokens from email links. Template: page-unsubscribe.php.
Contact (/contact/) Template: page-no-sidebar.php. Fields: Name, Email, Subject, Message. AJAX submission. Honeypot field for spam protection.
4. Design System
4.1 Color Palette
Navy blue #1B3A6B Primary brand <20> headers, nav bar, section banners. Customizer-controlled (primary_color).
Slate blue #2E6DA4 Heading text, links, active states. Fixed, derived from primary_color.
Sky blue #4A90D9 Hover states, accent highlights. Fixed.
Warm gold #C8A96E Decorative accents, active nav underline, dividers. Customizer-controlled (accent_color).
Cream #F5F0E8 Page background. Customizer-controlled via custom-background (default-color: F5F0E8).
Parchment #EDE4D0 Card and sidebar backgrounds, alternating table rows. Fixed.
Body text #2C2C2C Primary body copy. Fixed.
Mid grey #555555 Secondary text, metadata. Fixed.
Light grey #888888 Placeholder text, disabled states. Fixed.
Active green #2D6A4F Format toggle active state. Fixed.
Note: Colors output as CSS custom properties: --bdm-navy, --bdm-gold, --bdm-cream, etc. Light mode only <20> no dark mode.
4.2 Typography
Heading font Playfair Display, Georgia, serif. Customizer-controlled (heading_font).
Body font Lato, Calibri, sans-serif. Customizer-controlled (body_font).
Navigation Lato, Calibri <20> medium weight, small caps optional. Fixed.
Pull quotes / blurbs Playfair Display italic. Inherits heading_font.
Short links / code display Courier New, monospace. Fixed.
Note: Google Fonts loaded via wp_enqueue_style() with display=swap.
4.3 Image Sizes
shelf-thumb 44<34>62px Book shelf widget covers on home page right column.
blog-thumb 100<30>68px Blog post list featured image thumbnails.
rev-thumb 52<35>74px Review page book cover thumbnails.
book-cover 130<33>185px All books page cover images.
book-page-cover 220<32>310px Individual book page main cover.
series-card 400<30>280px Series listing page card graphics.
series-header 1200<30>300px Individual series page header graphic.
Note: All sizes registered via add_image_size() in functions.php. WordPress generates crops automatically on upload.
4.4 Responsive Breakpoints
? 1200px Full two-column layouts, full banner, desktop navigation bar.
768<EFBFBD>1199px Columns stack to single column. Navigation collapses to hamburger menu.
< 768px Single column, touch-friendly tap targets (minimum 44<34>44px). Banner scales proportionally.
Note: Sticky nav functions at all breakpoints. On mobile, hamburger menu opens below the sticky nav bar.
5. Plugin: BDMurphy-BookShelf
The BD-BookShelfHelper plugin is documented in a separate specification: BD-BookShelfHelper-Plugin-Specification-v1_0.docx. This section covers the BDMurphy-BookShelf plugin.
5.1 Overview
The BDMurphy-BookShelf plugin handles subscriber sign-up and management, email newsletter creation, AI-assisted newsletter composition, and delivery via Gmail SMTP. It also registers the book shelf sidebar widget on the home page right column and the /signup/ registration page.
5.2 Plugin Identity
Plugin name BDMurphy-BookShelf
WordPress slug bdmurphy-bookshelf
Dashboard menu label BookShelf
Text domain bdmurphy-bookshelf
5.3 Responsibilities
<20> Home page book shelf sidebar widget <20> registered and rendered in the "Home Sidebar" widget area.
<20> Sign-up page (/signup/) <20> registration form, opt-in category selection, subscriber storage.
<20> Subscriber management <20> subscribed / unsubscribed status, category preferences, audit fields.
<20> Email newsletter composition <20> AI-assisted drafting via Gemini API, HTML editor, template selection.
<20> Email delivery <20> one at a time via Gmail SMTP using an app-specific password.
<20> Unsubscribe processing <20> HMAC-signed one-click tokens and manual /unsubscribe/ form.
<20> CAN-SPAM compliance <20> mandatory footer with business address pulled from Theme Options.
5.4 Gmail SMTP Configuration
Email address Full email address used for sending.
App-specific password Generated in Google Account > Security. Stored encrypted in wp_options.
Sender display name Display name, e.g. "B.D. Murphy".
Reply-to address Can differ from sender address.
AI API key Gemini API key for AI-assisted newsletter composition. Stored encrypted.
AI link Used for AI to check and help craft newsletters and emails.
Business information Pulled automatically from Appearance > Theme Options > Business Information for CAN-SPAM compliance.
Site URL (boilerplate) Auto-populated from get_bloginfo('url').
SMTP configuration Configuration screen includes help text and links to Gmail app password setup guide.
5.5 Email Newsletter Composition Workflow
<20> Author opens BookShelf > New Newsletter.
<20> Author enters a prompt summary: topic, books to mention, tone, and announcements.
<20> Plugin calls Gemini API with a system instruction to generate a complete newsletter HTML draft.
<20> Draft is returned in an HTML email editor. Headers and footers are auto-populated above and below the editor so the author sees the full reader view.
<20> Author reviews and edits the draft content.
<20> Author checks "Also publish as blog post" <20> creates a WordPress post with the same content. This checkbox is ON by default.
<20> Author clicks "Send Newsletter" <20> sends one email at a time via Gmail SMTP to all subscribed recipients.
Note: Sending is always manually triggered. There is no automatic or scheduled send.
5.6 Email Template Requirements
<20> Email template is selectable in BookShelf settings.
<20> Header: site name / author name and logo.
<20> Body: composed content with affiliate-coded purchase links.
<20> Mandatory footer (not editable per send): website URL, one-click unsubscribe link (HMAC-signed token), full business address from Theme Options (CAN-SPAM requirement).
5.7 Subscriber Management
id Auto-increment primary key.
email Unique subscriber email address.
first_name Optional first name.
status "subscribed" or "unsubscribed" <20> records are never deleted.
subscribed_categories Newsletter, Announcements (new books), Events <20> per-subscriber opt-in flags.
subscribed_at Timestamp of original opt-in.
unsubscribed_at Timestamp of unsubscribe (null if still subscribed).
ip_address IP address at sign-up for audit purposes.
One-click unsubscribe: HMAC-signed token in every email link. Clicking the link sets status = "unsubscribed" immediately, no form required. Satisfies Gmail and Yahoo delivery requirements. The /unsubscribe/ page also accepts manual email entry as a fallback.
5.8 Plugin Settings & Admin Dashboard
The plugin adds a top-level menu item to the WordPress dashboard called "BookShelf". Submenus provide access to all features.
BookShelf > Settings Gmail credentials (email address, app-specific password), sender display name, reply-to address, Gemini API key, email template selection. Business information is pulled from Theme Options and displayed read-only here.
BookShelf > New Newsletter AI-assisted email composition screen.
BookShelf > Sent Newsletters Log of previously sent newsletters with subject, sent date, and recipient count.
BookShelf > Subscribers Subscriber list with status, category preferences, and sign-up date.
BookShelf > About Plugin version, author credit, Buy Me a Coffee link, PayPal donate link.
6. Database Schema (Overview)
6.1 BD-BookShelfHelper Tables
Full detail in the BD-BookShelfHelper Plugin Specification, Section 11.
bd_books Core book record (id, slug, pen_name_id, series_id, volume_number, asin, isbn, publish_date, sort_order, created_at).
bd_book_meta Per-language content (book_id, language_code, title, blurb, preview_excerpt, pull_quotes).
bd_book_covers Cover images per language (book_id, language_code, attachment_id).
bd_series Series definitions (id, name, slug, cover_attachment_id, header_attachment_id, blurb, display_order, created_at).
bd_pen_names Pen name list (id, pen_name, notes, active, created_at).
bd_retailers Retailer definitions (id, name, url_pattern, affiliate_param, formats_supported).
bd_book_links Purchase links (book_id, retailer_id, format, raw_url, affiliate_url, last_verified).
bd_reviews Review records (id, book_id, comment_text, synopsis, source_name, source_url, display_order).
bd_ratings Scraped ratings cache (book_id, source [amazon|goodreads], rating, review_count, last_updated).
bd_review_shortlinks Review short links (id, book_id, site_code, slug, destination_url, status, created_at, activated_at).
6.2 BDMurphy-BookShelf Tables
bd_subscribers Subscriber list (id, email, first_name, status, subscribed_categories, subscribed_at, unsubscribed_at, ip_address).
bd_newsletters Sent newsletter log (id, subject, body_html, sent_at, recipient_count).
7. Design Decisions <20> Answered
All design decisions have been resolved. This section records each decision and its specification impact for developer reference.
Question Answer Spec Impact
Child theme support? No <20> customisation via Customizer and Theme Options only. All paths use get_template_directory_uri(). No child theme infrastructure needed.
Dark mode? No dark mode support. Light mode only confirmed. Parchment palette stands.
Sign-up form <20> popup or page? Dedicated page (/signup/) accessed from About menu and footer. Section 3.8. About page and footer include sign-up prompt. Shortcode [bookshelf_signup] also available.
Visitor-submitted reviews? No <20> admin-entered only. Reviews come from Amazon, Goodreads, etc. Admin-only entry. No moderation tooling needed.
Blog post trigger email newsletter? No <20> future separate AI-driven plugin (blog ? email / social, not a copy). Section 8.4 notes the planned extension and reserves a WordPress action hook.
Social sharing buttons? Yes <20> bottom of blog posts and book pages. Section 3.9. Facebook, X/Twitter, LinkedIn, WhatsApp, copy-link. Book pages share the short URL.
Series page? Yes <20> full series system with /series/ listing and /series/{slug}/ detail pages. Section 3.7. BD-BookShelfHelper series management. Series nav item conditional on series existing.
Affiliate IDs? User adds IDs as they sign up. Applied whenever available. Links display without affiliate code until an ID is configured for that retailer.
CAN-SPAM mailing address? User configures business information (name, address, phone) in Theme Options. Business info fields in Section 2.5. BDMurphy-BookShelf plugin pulls them into every email footer.
One-click unsubscribe? Yes <20> HMAC-signed token in email link. Section 5.7. Token-based unsubscribe confirmed as standard.
Goodreads Author ID? ID: 30469651 (goodreads.com/author/show/30469651.B_D_Murphy) Pre-populated in BD-BookShelfHelper sample data.
Multiple authors / pen names? One author, one bio. Up to 10 pen names. Each book shows the pen name it was published under. BD-BookShelfHelper pen name management (Plugin Spec Section 4). About page bio is for the author, not per pen name.
Series sort order? Volume number field controls both display order and the "Book [N]" label. Volume number in bd_books. Sorts books within a series on all series pages.
Email plugin name? The BD-SimpleEmail plugin is merged into BDMurphy-BookShelf. Email features are part of that plugin. All references to "SimpleEmail" or "BD-SimpleEmail" in this document now refer to the BDMurphy-BookShelf plugin.
8. Implementation Notes
8.1 WordPress Architecture
<20> "book" custom post type registered by BD-BookShelfHelper. The theme queries it but does not register it.
<20> "series" registered as a custom taxonomy by BD-BookShelfHelper, attached to the book CPT.
<20> Book page short links and review short links registered as WordPress rewrite rules via add_rewrite_rule(). Slugs stored as post meta and in bd_review_shortlinks.
<20> Internationalisation: theme uses __() and _e() with text domain "bd-murphy". load_theme_textdomain() in functions.php. .pot generated via WP-CLI i18n make-pot.
<20> Scheduled tasks via WP-Cron: Goodreads widget refresh (daily), Amazon rating scrape (every 6 hours), review link activation scan (daily).
<20> All CSS and JS enqueued via wp_enqueue_style() and wp_enqueue_script() with correct handles, dependencies, and version strings.
8.2 Security
<20> Gmail app password and Gemini API key stored encrypted (openssl_encrypt), never plain text.
<20> Unsubscribe tokens: HMAC-signed (hash_hmac) <20> never bare email addresses in URLs.
<20> All output escaped with esc_html(), esc_url(), esc_attr(). All input sanitized on save.
<20> Affiliate codes applied server-side only.
<20> Contact form honeypot field for spam protection.
8.3 Sample Data
<20> "Pandemic Hacker" by B.D. Murphy <20> placeholder cover, sample reviews, ratings, blurb.
<20> Pre-reserved review short link slugs: phrA, phrG, phrB, phrK, phrP, phrL.
<20> All five retailer purchase link placeholders pre-configured for Pandemic Hacker.
<20> Three sample blog posts.
<20> Goodreads Author ID 30469651 pre-populated in BD-BookShelfHelper settings.
<20> Pen name "B.D. Murphy" pre-configured as the default active pen name.
Note: A "Clear sample data" button in Appearance > Setup Wizard removes all sample content.
8.4 Planned Extension <20> Blog to Social / Email Plugin
A future AI-driven plugin will take a published blog post and generate distinct email newsletter content and social media posts <20> not a copy of the blog, but an AI-transformed version appropriate for each channel.
<20> Scope: out of scope for the current build. No implementation required now.
<20> Hook point reserved: the theme and BD-BookShelfHelper fire do_action('bdm_post_published', $post_id) when a book or blog post is published. The future plugin attaches to this hook without requiring changes to the theme or existing plugins.
<20> The BDMurphy-BookShelf plugin (current) handles one-way manual email composition only. The future plugin is separate.
8.5 Required Theme Support Declarations
All required add_theme_support() calls in functions.php:
title-tag Lets WordPress manage the <title> element. Not hard-coded in header.php.
custom-logo array( 'width' => 250, 'height' => 250, 'flex-width' => true, 'flex-height' => true )
custom-header array( 'default-image' => get_template_directory_uri().'/assets/Reading_through_time.png', 'width' => 1200, 'height' => 250, 'flex-height' => false )
custom-background array( 'default-color' => 'F5F0E8' )
post-thumbnails No args. Required for featured images.
html5 array( 'search-form', 'comment-form', 'comment-list', 'gallery', 'caption', 'script', 'style' )
automatic-feed-links No args. Adds RSS/Atom links to head.
wp-block-styles No args. Applies default Gutenberg block styles.
editor-style add_editor_style('assets/editor-style.css').
add_image_size shelf-thumb 44<34>62 (crop), blog-thumb 100<30>68 (crop), rev-thumb 52<35>74 (crop), book-cover 130<33>185 (crop), book-page-cover 220<32>310 (crop), series-card 400<30>280 (crop), series-header 1200<30>300 (crop).
8.6 Template File Structure
style.css Theme header: Theme Name: BDMurphy-Bookshelf, Author, Version, Text Domain: bd-murphy.
functions.php add_theme_support(), register_nav_menus(), register_sidebar(), wp_enqueue_scripts, Customizer panels, Theme Options, bdm_post_published action hook.
header.php wp_head(), banner image, sticky nav, body_class().
footer.php Footer three-zone layout, sign-up prompt, wp_footer().
front-page.php Home page two-column layout.
sidebar-home.php Home right column <20> book shelf widget area.
archive.php / home.php Blog post list.
archive-book.php All books page.
single-book.php Individual book page (series display and social sharing).
archive-series.php Series listing page (/series/).
taxonomy-series.php Individual series page (/series/{slug}/).
page.php Default page template with sidebar.
page-full-width.php Full width. Template Name: Full Width.
page-no-sidebar.php Single column. Template Name: No Sidebar.
page-about.php About page. Template Name: About.
page-reviews.php Reviews grid. Template Name: Reviews.
page-unsubscribe.php Unsubscribe form. Template Name: Unsubscribe.
page-signup.php Sign-up page. Template Name: Sign Up.
404.php 404 error page.
search.php Search results.
8.7 Required Template Tag Usage
<20> body_class() on body tag <20> plugin hooks and admin bar CSS offset.
<20> post_class() on post/book wrapper divs <20> plugin hooks and state classes.
<20> wp_head() immediately before closing </head> <20> required for all plugins.
<20> wp_footer() immediately before closing </body> <20> required for all plugins.
<20> the_custom_logo() for logo output.
<20> get_header_image() for banner image output.
<20> wp_nav_menu(['theme_location' => 'primary']) for main nav.
<20> wp_nav_menu(['theme_location' => 'footer']) for footer links.
<20> dynamic_sidebar('home-sidebar') for the book shelf widget area.
<20> do_action('bdm_post_published', $post_id) fired on book and blog post publish.
End of BDMurphy BookShelf System Specification v2.0
Note: For the BD-BookShelfHelper plugin full specification, see: BD-BookShelfHelper-Plugin-Specification-v1_0.docx

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,716 @@
# BDMurphy BookShelf Template - Technical Specification v2.2.10
**WordPress Theme:** BDMurphy BookShelf
**Plugin 1:** BD-BookShelfHelper (not yet implemented)
**Plugin 2:** BD-BookShelf (not yet implemented)
**Version:** 2.2.10 | June 2026
**Status:** Theme fully implemented. Plugins pending development.
---
## Table of Contents
1. [Project Overview](#1-project-overview)
2. [Theme - Global Layout](#2-theme---global-layout)
3. [Theme - Pages](#3-theme---pages)
4. [Design System](#4-design-system)
5. [Goodreads Widget (Theme-Built)](#5-goodreads-widget-theme-built)
6. [Plugin: BD-BookShelfHelper (Planned)](#6-plugin-bd-bookshelfhelper-planned)
7. [Plugin: BD-SimpleEmail (Planned)](#7-plugin-bd-simpleemail-planned)
8. [Implementation Standards](#8-implementation-standards)
9. [Current Build Status](#9-current-build-status)
---
## 1. Project Overview
### 1.1 Purpose
This specification defines a custom WordPress theme and two companion plugins for author B.D. Murphy's website. The system allows the author to showcase books, publish blogs, manage reviews, and send newsletters with minimal technical knowledge required after initial setup.
**Theme Name:** BDMurphy Bookshelf
**Plugin 1:** BD-BookShelfHelper
**Plugin 2:** BD-SimpleEmail
### 1.2 Deliverables
- **WordPress Theme:** BDMurphy Bookshelf — custom theme, fully Customizer-driven
- **Plugin: BD BookShelfHelper:** Book data, series, pen names, short links, review links, SEO, and platform integrations. Book data stored and managed in one place accessable everywhere. *(Planned)*
- **Plugin: BD SimpleEmail:** Subscriber management and AI-assisted newsletter delivery *(Planned)*
### 1.3 Design Identity
| Element | Specification |
|---------|--------------|
| Logo | Quill-pen hybrid icon with "B.D. Murphy" in serif text file: "Icon_Quill_BDMurphy_Black_WhtBG.jpg" |
| Banner | Illustrated timeline banner (1538x250px), full-width on all pages |
| Primary Color | Navy blue (#1B3A6B) |
| Accent Color | Warm gold (#C8A96E) |
| Background | Parchment (#F5F0E8) |
| Heading Font | Playfair Display / Georgia fallback |
| Body Font | Lato / Calibri fallback |
### 1.4 General Behavior
All books and blog content displays in reverse chronological order by first-published date unless a manual sort position is assigned. Series content sorts by volume number within the series. UI chrome follows the site language setting; content stays in the language it was authored in.
### 1.5 WordPress Coding Standards Compliance
All code complies with [WordPress Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/):
- **PHP:** PSR-12 compatible with WordPress conventions
- **JavaScript:** ES5+ with JSHint validation
- **CSS:** WordPress CSS coding standards
- **HTML:** Semantic HTML5, no inline styles except where unavoidable
- **Text Domain:** `bd-murphy` for theme, `bd-bookshelfhelper` and `bd-simpleemail` for plugins
---
## 2. Theme - Global Layout
### 2.1 Header
- Banner image (1538x250px) displayed at 100% viewport width on all pages
- Default asset: `assets/images/banner.jpg`
- Site logo via `add_theme_support('custom-logo')`
- Default logo asset: `assets/images/Icon_Quill_BDMurphy_Black_WhtBG.jpg`
- Admin bar offset handled via `body.admin-bar` rules
### 2.2 Navigation Menu
- Registered via `register_nav_menus(['primary' => 'Primary Menu', 'footer' => 'Footer Menu'])`
- **Default menu items:** Home, Books, Blogs, Reviews, About
- Sticky behavior with gold top border on scroll
- Hamburger menu on mobile (<768px)
- Custom link fallback when no menu assigned
### 2.3 Footer
- Three-zone layout: social icons, donate/utility links, footer links
- **Footer links displayed as vertical list** (not jumbled)
- Two widget areas: "Footer Left" and "Footer Right"
- Sign-up prompt with link to /signup/ page (configurable show/hide)
- Copyright notice with dynamic year
### 2.4 WordPress Customizer Settings
| Setting | Function | Location |
|---------|----------|----------|
| Site title | `get_bloginfo('name')` | Settings > General |
| Logo | `the_custom_logo()` | Customizer > Site Identity |
| Banner | `get_header_image()` | Customizer > Header Image |
| Background | Custom background | Customizer > Background |
| Primary color | `get_theme_mod('primary_color', '#1B3A6B')` | Customizer > Colors |
| Accent color | `get_theme_mod('accent_color', '#C8A96E')` | Customizer > Colors |
| Heading font | `get_theme_mod('heading_font', 'Playfair Display')` | Customizer > Typography |
| Body font | `get_theme_mod('body_font', 'Lato')` | Customizer > Typography |
| Author name | `get_theme_mod('author_name', 'B.D. Murphy')` | Customizer > Author Book Settings |
| Goodreads ID | `get_theme_mod('goodreads_author_id', '30469651')` | Customizer > Author Book Settings |
| Amazon Author ID | `get_theme_mod('amazon_author_id', '30469651')` | Customizer > Author Book Settings |
| Show Newsletter Signup | `get_theme_mod('show_newsletter_signup', true)` | Customizer > Display Options |
| Show Sign Up Link | `get_theme_mod('show_signup_link', true)` | Customizer > Display Options |
| Social Sharing | `get_theme_mod('share_facebook', true)` etc. | Customizer > Social Sharing |
### 2.5 Theme Options Admin Screen
Settings saved via WordPress Settings API in `bdm_theme_options`:
- Social media URLs (X/Twitter, Facebook, LinkedIn, Instagram)
- Donation URLs (Buy Me a Coffee, PayPal)
- Author photo (media attachment ID)
- Sticky navigation toggle
- Posts per page
- Show read time toggle
- Business information (for CAN-SPAM compliance)
---
## 3. Theme - Pages
### 3.1 Home Page (front-page.php)
- Banner at 100% screen width
- Navigation menu line (Home, Books, Blogs, Reviews, About)
- Two-column layout: 66% content, 34% sidebar
- Content via WordPress block editor
- Sidebar contains Goodreads Books widget (fetches from Goodreads API)
### 3.2 Blog Page (/blog/)
Template: `archive.php` or `home.php`. Lists posts with title, date, category, excerpt, featured image. Paginated (10 per page). Social sharing on individual posts.
### 3.3 Books Page (/books-2/)
Template: `page-books.php`. Grid layout of book covers matching authorbdmurphy.com style.
**Default behavior (no plugin):** Shows default example books with covers from uploaded images:
- Pandemic Hacker
- Pandemic Hacker 2
- The Last Dwarf
- Deal Hunter
- Nanite Evolution
- Sidney and Watson: Time Detectives
**With BD BookHelper plugin:** Shows books from the `book` custom post type.
**No sidebar on Books page** (correct behavior per specification).
### 3.4 Individual Book Page (single-book.php)
Full-width template with:
- Language selector tabs (EN, FR, DE, ES, RU, ZH-S, IT, PT, JA)
- Cover image (220x310px)
- Title, pen name, series info
- Ratings, format toggle, retailer links
- Read preview, pull quotes, reviews
- Share button (copies short URL)
- Social sharing buttons
**Short Link Access:** Books are accessible via short URLs using the slug defined in book metadata. URL pattern: `/{short-slug}` (e.g., `/ph/` for "Pandemic Hacker")
### 3.5 Reviews Page (/reviews/)
Template: `page-reviews.php`. Two-column layout with review cards and Goodreads sidebar widget.
### 3.6 About Page (/about/)
Template: `page-about.php`. Two-column layout:
- **Left column:** Author bio content, awards section (if plugin active), donate buttons, signup prompt
- **Right column (sidebar):**
- Author photo (thumbnail size, circular crop)
- Newsletter signup card (configurable show/hide)
- **Goodreads Books widget** (below author photo, shows on all pages except Books)
### 3.7 Series Pages
- Listing: `archive-series.php` at /series/
- Individual: `taxonomy-series.php` or `single-series.php` at /series/{slug}/
- Series nav item hidden when no series exist
### 3.8 Sign-up Page (/signup/)
Template: `page-signup.php`. Email, first name, opt-in checkbox. Shortcode `[bdm_signup]` available.
### 3.9 Social Sharing Buttons
Platforms: Facebook, X/Twitter, LinkedIn, WhatsApp, Copy Link. Uses Web Share API where available. Book pages share the short URL.
### 3.10 Utility Pages
- Privacy Policy: standard WordPress page
- Unsubscribe: single email input + one-click HMAC tokens
- Contact: AJAX form with honeypot spam protection
- 404: Custom error page with home link
---
## 4. Design System
### 4.1 Color Palette (CSS Custom Properties)
```css
:root {
--bdm-navy: #1B3A6B;
--bdm-slate: #2E6DA4;
--bdm-sky: #4A90D9;
--bdm-gold: #C8A96E;
--bdm-cream: #F5F0E8;
--bdm-parchment: #EDE4D0;
--bdm-body-text: #2C2C2C;
--bdm-mid-grey: #555555;
--bdm-light-grey: #888888;
--bdm-active-green: #2D6A4F;
}
```
### 4.2 Typography
- Headings: Playfair Display, Georgia, serif
- Body: Lato, Calibri, sans-serif
- Google Fonts loaded via `wp_enqueue_style()` with `display=swap`
### 4.3 Image Sizes
| Size | Dimensions | Use |
|------|-----------|-----|
| shelf-thumb | 44x62px (crop) | Book shelf widget |
| blog-thumb | 100x68px (crop) | Blog post thumbnails |
| rev-thumb | 52x74px (crop) | Review page covers |
| book-cover | 130x185px (crop) | All books page |
| book-page-cover | 220x310px (crop) | Individual book page |
| series-card | 400x280px (crop) | Series listing |
| series-header | 1200x300px (crop) | Series header |
### 4.4 Responsive Breakpoints
| Breakpoint | Layout |
|------------|--------|
| >=1200px | Full two-column, desktop nav |
| 768-1199px | Single column, hamburger menu |
| <768px | Single column, touch-friendly targets (44x44px min) |
### 4.5 Banner Behavior
- Full viewport width on all pages using `width: 100vw; margin-left: calc(-50vw + 50%)`
- Max height: 200px with object-fit: cover
- Consistent across all page templates
---
## 5. Goodreads Widget (Theme-Built)
### 5.1 Overview
The Goodreads widget is built directly into the theme (not dependent on plugins). It fetches books from the author's Goodreads page and displays them in the sidebar.
### 5.2 Widget Behavior
- **Widget name:** BDM_Goodreads_Books_Widget
- **Widget title:** "GoodReads for {author_name}" (configurable)
- **Sidebar assignment:** `home-sidebar` (used on all pages except Books)
### 5.3 Data Sources
1. **BD BookHelper Plugin (if active):** Queries `book` CPT for local book data
2. **Goodreads Fallback (no plugin):** Fetches from `https://www.goodreads.com/author/list/{author_id}`
### 5.4 Goodreads Integration Details
- **Author ID:** 30469651 (configurable via Customizer)
- **Fetch method:** `wp_remote_get()` with 10-second timeout
- **HTML parsing:** DOMDocument with XPath queries
- **Data extracted:** Title, cover image, URL, rating
- **Sort order:** By review count descending (most reviews first)
- **Max books displayed:** 6
### 5.5 Widget Display
```html
<div class="goodreads-books-list">
<div class="goodreads-book-item">
<div class="goodreads-book-cover">
<img src="..." alt="Book Title" loading="lazy">
</div>
<div class="goodreads-book-details">
<a href="..." class="goodreads-book-title">Book Title</a>
<div class="goodreads-book-ratings">
<span class="rating goodreads">Goodreads: 4.38/5</span>
</div>
</div>
</div>
</div>
```
### 5.6 Page Visibility
| Page | Widget Shown |
|------|--------------|
| Home | Yes |
| Books | **No** (correct behavior) |
| Blogs | Yes |
| Reviews | Yes |
| About | Yes (under author photo) |
| Individual Book | Yes |
| Individual Review | Yes |
| Contact | Yes |
| Signup | Yes |
| 404 | No |
---
## 6. Plugin: BD-BookShelfHelper (Planned)
### 6.1 Overview
Manages all book-related data, series information, and pen name configuration. Registers "book" custom post type and "series" taxonomy.
### 6.2 Book Data Entry
**6.2.1 Data Entry Interface**
The plugin provides a dedicated book data entry page with the following UI requirements:
- **Input Fields:** Each data field has a dedicated input control (text, textarea, select, date picker as appropriate)
- **Labels:** Every field has a visible label using `<label>` elements properly associated via `for` attribute
- **Hints:** Helper text below each field provides context, format requirements, and examples
- **Validation:**
- Real-time validation on blur with visual feedback (green checkmark for valid, red border + message for invalid)
- Server-side validation on save with error messages displayed above problematic fields
- Required fields marked with asterisk and validated before submission
- Format validation: ISBN-13 (13 digits), ASIN (10 alphanumeric), dates (YYYY-MM-DD), URLs (valid format)
- **Organization:** Fields grouped into logical sections with collapsible headers:
- Basic Information (title, pen name, publication date)
- Identifiers (ISBN-13, ASIN)
- Series & Order (series selection, volume number)
- Content (blurb, preview excerpt, pull quotes)
- Metadata (genre tags, Amazon search terms)
- Retailer Links (Amazon, Apple, Google, B&N, Kobo)
- Reviews (add/edit reviews)
- **Multi-language Tabs:** Language selector tabs for entering content in each supported language
- **Auto-save Draft:** Automatic saving of work in progress every 60 seconds
- **Preview:** Button to preview how the book will appear on the front-end
**Core Fields (language-independent):**
- Cover image (per language)
- Pen name (from configured list)
- Publication date, sort order
- ISBN-13, ASIN
- Series, volume number
- Short link slug
**Content Fields (per language):**
- Title, blurb, preview excerpt
- Pull quotes (up to 5)
- Genre tags (min 3)
- Amazon search terms
**Reviews (per book):**
- Comment text, source name, source URL
- AI-generated 3-line synopsis
- Display order
**Supported Languages:** EN, FR, ES, DE, IT, PT, RU, JA, ZH (Simplified)
### 6.3 Series Management
- Series name, slug, cover graphic, blurb, display order
- Book series assignment with volume number
- Conditional nav item rendering
### 6.4 Pen Name Management
- Up to 10 pen names
- Active/inactive toggle
- Notes field (internal only)
- Per-book assignment
### 6.5 Retailer Purchase Links
| Retailer | Formats | URL Pattern |
|----------|---------|-------------|
| Amazon | Ebook, Paperback, Hardback | `amazon.com/dp/{ASIN}?tag={affiliate_id}` |
| Apple Books | Ebook | URL with `at=` parameter |
| Google Play | Ebook | URL with partner code |
| Barnes & Noble | All | URL with affiliate parameter |
| Kobo | Ebook | URL with affiliate parameter |
### 6.6 Short Link System
**Book Short Links:**
- Auto-generated from first letter of significant words
- Examples: "Pandemic Hacker" → `/ph/`, "Deal Hunter" → `/dh/`
- Conflict check with incrementing numbers
**Review Short Links:**
- Pattern: `{book-slug}r{site-code}` (e.g., `dhrG` = Deal Hunter Goodreads)
- Lifecycle: Reserved → Pending → Active
### 6.7 Goodreads Integration
- Daily WP-Cron refresh
- Parses widget HTML for book data
- Author ID: 30469651
### 6.8 Amazon Ratings
- 6-hour WP-Cron scrape
- Staleness indicator on failure
- **Warning:** May conflict with Amazon Terms of Service
### 6.9 SEO Generation
Auto-generated on book save:
- Title tag, meta description
- Open Graph, Twitter Card tags
- schema.org Book structured data
### 6.10 Admin Menu Structure
```
BD BookHelper (top-level menu)
├── All Books
├── Add Book
├── Data Entry (Enhanced interface per 5.2.1)
├── Series (taxonomy submenu)
├── Pen Names
├── Settings
│ ├── Affiliate Codes
│ ├── Goodreads
│ └── Amazon Scrape
└── Migrate (Import from BD Books plugin)
```
### 6.11 Shortcodes
The plugin will provide WordPress shortcodes for displaying book content:
| Shortcode | Description |
|-----------|-------------|
| `[bd-book book="1"]` | Display single book by ID or title |
| `[bd-books]` | Display all books in a list |
### 6.12 Migration from BD Books Plugin
The plugin includes a migration tool to import books from the original "BD Books" (bd-book-listing) plugin:
**Migration Process:**
1. Reads from `{$wpdb->prefix}bd_books` table
2. Creates new book CPT entries
3. Maps author to pen names (creates new if needed)
4. Imports descriptions as blurbs (English)
5. Imports cover images (WordPress media or external URLs)
6. Parses and maps purchase links to retailer fields
---
## 7. Plugin: BD-SimpleEmail (Planned)
### 7.1 Overview
Handles email newsletter creation, subscriber management, and delivery via Gmail SMTP. AI-assisted composition via Gemini API.
### 7.2 Configuration
| Setting | Storage |
|---------|---------|
| Gmail address | `wp_options` |
| App password | Encrypted (openssl_encrypt) |
| Sender name | `wp_options` |
| Reply-to | `wp_options` |
| Gemini API key | Encrypted `wp_options` |
### 7.3 Email Composition Workflow
1. User opens "New Newsletter"
2. Enters subject and AI prompt (optional)
3. Plugin generates HTML via Gemini API
4. User edits draft
5. Optional: publish as blog post
6. Click "Send Newsletter" → sends to all subscribers via Gmail SMTP
### 7.4 Email Template
**Header:** Site name/logo
**Body:** Composed content with affiliate links
**Footer (mandatory):**
- Website URL
- One-click unsubscribe (HMAC-signed token)
- Full business address (CAN-SPAM)
- Unsubscribe text link
### 7.5 Subscriber Management
**Schema:**
```sql
bd_subscribers (
id PRIMARY KEY AUTO_INCREMENT,
email UNIQUE NOT NULL,
first_name VARCHAR(100),
status ENUM('subscribed', 'unsubscribed'),
subscribed_at DATETIME,
unsubscribed_at DATETIME NULL,
ip_address VARCHAR(45),
UNSUBSCRIBE_TOKEN VARCHAR(64) -- pre-generated
)
```
**One-Click Unsubscribe:**
- HMAC-signed token in every email
- Token format: `hash_hmac('sha256', email, AUTH_KEY)`
- Satisfies Gmail/Yahoo delivery requirements
### 7.6 Admin Menu Structure
```
BD SimpleEmail (top-level menu)
├── Subscribers
├── New Newsletter
└── Settings
└── Gmail Configuration
└── Gemini API
└── About
```
---
## 8. Implementation Standards
### 8.1 WordPress Architecture
1. **Custom Post Types:** "book" CPT registered by BD BookHelper (theme queries, doesn't register)
2. **Taxonomies:** "series" registered by BD BookHelper, attached to book CPT
3. **Rewrite Rules:** Short links via `add_rewrite_rule()`
4. **Internationalization:** Text domains (`bd-murphy`, `bd-bookshelfhelper`, `bd-simpleemail`), `load_theme_textdomain()` in functions.php
5. **Scheduled Tasks:** WP-Cron for Goodreads (daily), Amazon ratings (6 hours), review links (daily)
6. **Asset Loading:** All CSS/JS via `wp_enqueue_style()` and `wp_enqueue_script()` with handles, dependencies, version strings
### 8.2 Security Requirements
| Requirement | Implementation |
|-------------|----------------|
| Secrets storage | `openssl_encrypt()` with AES-256-CBC, key from AUTH_KEY |
| Unsubscribe tokens | `hash_hmac('sha256')` with AUTH_KEY |
| Output escaping | `esc_html()`, `esc_url()`, `esc_attr()`, `wp_kses_post()` |
| Input sanitization | `sanitize_text_field()`, `sanitize_email()`, `absint()`, etc. |
| CSRF protection | `wp_verify_nonce()` on all form submissions |
| Nonce verification | Nonce field + verification on every AJAX and form POST |
| Affiliate codes | Server-side application only, never in client-side code |
| Spam protection | Contact form honeypot field |
### 8.3 Required Theme Support Declarations
```php
add_theme_support('title-tag');
add_theme_support('custom-logo', array(
'width' => 200,
'height' => 200,
'flex-width' => true,
'flex-height' => true,
));
add_theme_support('custom-header', array(
'default-image' => BDM_URI . '/assets/images/banner.jpg',
'width' => 1200,
'height' => 200,
'flex-height' => false,
));
add_theme_support('custom-background', array(
'default-color' => 'F5F0E8',
));
add_theme_support('post-thumbnails');
add_theme_support('html5', array(
'search-form', 'comment-form', 'comment-list',
'gallery', 'caption', 'script', 'style'
));
add_theme_support('automatic-feed-links');
add_theme_support('wp-block-styles');
add_editor_style('assets/editor-style.css');
add_image_size('shelf-thumb', 44, 62, true);
add_image_size('blog-thumb', 100, 68, true);
add_image_size('rev-thumb', 52, 74, true);
add_image_size('book-cover', 130, 185, true);
add_image_size('book-page-cover', 220, 310, true);
add_image_size('series-card', 400, 280, true);
add_image_size('series-header', 1200, 300, true);
```
### 8.4 Template File Structure (Current Build)
```
bd-murphy-author/
├── style.css (Theme header: Name, Author, Version 2.2.0, Text Domain)
├── functions.php (add_theme_support, register_nav_menus, widgets, etc.)
├── header.php (wp_head, banner, nav, body_class())
├── footer.php (Footer zones, wp_footer, vertical links)
├── front-page.php (Home page with sidebar)
├── archive.php (Blog posts)
├── archive-book.php (All books - plugin)
├── archive-series.php (Series listing - plugin)
├── single-book.php (Individual book - plugin)
├── taxonomy-series.php (Individual series - plugin)
├── index.php (Fallback template)
├── page.php (Default page with sidebar)
├── page-about.php (About - author photo, newsletter, Goodreads widget)
├── page-books.php (Books grid - NO sidebar)
├── page-reviews.php (Reviews with sidebar)
├── page-full-width.php (Template: Full Width)
├── page-no-sidebar.php (Template: No Sidebar)
├── page-signup.php (Template: Sign Up)
├── page-unsubscribe.php (Template: Unsubscribe)
├── page-contact.php (Contact with AJAX)
├── 404.php (Custom error page)
├── search.php (Search results)
├── screenshot.png (Theme screenshot)
├── inc/
│ ├── customizer.php (Customizer settings, Display Options)
│ ├── theme-options.php (Theme Options admin page)
│ ├── template-tags.php (Helper functions, author photo thumbnail)
│ ├── widgets.php (Goodreads Books Widget)
│ ├── nav-menu.php (Custom menu walkers)
│ ├── short-urls.php (Short URL rewrite rules)
│ ├── theme-admin.php (Admin page functions)
│ ├── setup.php (Theme setup)
│ ├── book-helpers.php (Book helper functions)
│ ├── book-api.php (Book API functions)
│ └── ajax-handlers.php (AJAX handlers)
├── assets/
│ ├── css/
│ │ ├── style.css (Main stylesheet - 1740+ lines)
│ │ ├── editor-style.css
│ │ └── admin-style.css
│ ├── js/
│ │ ├── navigation.js (Menu toggle, sticky nav)
│ │ └── social-share.js
│ └── images/
│ ├── banner.jpg
│ └── Icon_Quill_BDMurphy_Black_WhtBG.jpg
└── template-parts/
└── social-share.php
```
### 8.5 Required Template Tag Usage
| Tag | Location | Purpose |
|-----|----------|---------|
| `body_class()` | `<body>` | Plugin hooks, admin bar offset |
| `post_class()` | Article wrappers | Plugin hooks, state classes |
| `wp_head()` | Before `</head>` | Plugin hooks, styles |
| `wp_footer()` | Before `</body>` | Plugin hooks, scripts |
| `the_custom_logo()` | Header | Logo output |
| `get_header_image()` | Header | Banner output |
| `wp_nav_menu()` | Header, Footer | Navigation menus |
| `dynamic_sidebar()` | Sidebar areas | Widget areas |
| `do_action('bdm_post_published', $post_id)` | On publish | Future plugin hook |
### 8.6 Hooks Reference
**Actions:**
- `bdm_post_published` — Fired on book/blog post publish (future plugin hook)
**Filters:**
- `bdm_nav_menu_items` — Filter for Series menu conditional
- `bdm_social_share_url` — Filter share URLs
- `bdm_email_footer_content` — Filter email footer
---
## 9. Current Build Status
### 9.1 Theme Implementation: COMPLETE
| Feature | Status | Notes |
|---------|--------|-------|
| Banner (full-width) | ✅ Complete | 1538x250px, responsive |
| Navigation menu | ✅ Complete | Home, Books, Blogs, Reviews, About |
| Sticky nav | ✅ Complete | Gold border on scroll |
| Home page | ✅ Complete | Two-column with Goodreads sidebar |
| Books page | ✅ Complete | Grid layout, no sidebar, default examples |
| About page | ✅ Complete | Author photo, newsletter, Goodreads widget |
| Reviews page | ✅ Complete | Two-column with sidebar |
| Blog page | ✅ Complete | Standard archive |
| Footer | ✅ Complete | Three-zone, vertical links |
| Customizer | ✅ Complete | Colors, fonts, display options, Goodreads ID |
| Goodreads widget | ✅ Complete | Fetches from Goodreads, shows on all pages except Books |
| Social sharing | ✅ Complete | Facebook, X, LinkedIn, WhatsApp, Copy Link |
| Contact form | ✅ Complete | AJAX with honeypot |
| Signup page | ✅ Complete | Email signup form |
| 404 page | ✅ Complete | Custom error page |
| Mobile responsive | ✅ Complete | Hamburger menu, touch targets |
| Default images | ✅ Complete | Bundled in assets/images/ |
### 9.2 Plugin Implementation: NOT STARTED
| Plugin | Status |
|--------|--------|
| BD-BookShelfHelper | ❌ Not implemented |
| BD-SimpleEmail | ❌ Not implemented |
### 9.3 Current Version
- **Theme Version:** 2.2.0
- **Zip File:** `/home/bdm/Bookshelf/bd-bookshelf-v2.2.0.zip`
- **Test Site:** https://bdmurphy.me (local at `/var/www/html/wordpress/`)
---
**End of Specification v2.2**

BIN
BDMurphyTemplate.zip Executable file

Binary file not shown.

31
BookDetailPage.md Executable file
View File

@@ -0,0 +1,31 @@
# Book Detail Page Requirements
1. **Language Selection Dropdown**
- **Location**: Top right of the page.
- **Data Source**: The dropdown will only display languages that are currently configured for the specific book in `bookshelfhelper` (e.g., checking if localized fields like `title_lang` or `blurb_lang` have data).
2. **Layout Structure**
- **Left Column**: The book cover remains on the left.
- **Under Left Column (Cover)**: The store/shopping selections will be moved here.
- **Central Column**:
- Book Title
- Author (Pen Name)
- Ratings (Amazon, Goodreads)
- Formats available (Ebook, Paperback, Hardcover, Audiobook)
- Book Blurb (HTML formatted to allow display formatting) will be positioned directly below the formats in the central column.
- **Right Column**: Theme-based right sidebar using the theme's dynamic sidebar. The default is to show Goodreads books the author has on the site.
3. **Format Selection & Store Links Interaction**
- **Functionality**: Selecting a format (Ebook, Paperback, Hardcover, audiobook) will dynamically update the store selections under the cover image.
- **Dynamic Styling**:
- Sites offering the selected format will be clickable and appear normal.
- Formats not available on specific sites will be grayed out/unclickable.
- **Source**: Store links are managed as part of the `bookshelfhelper` setup.
4. **URL Structure**
- When a book is added, the link will be based on the title acronym/short slug (e.g., "Pandemic Hacker" becomes "ph"). This is supported by the `bookshelfhelper` short link slug functionality.
5. **Implementation Scope**
- Build this detail page structure so that `bookshelfhelper` can render it properly.
- Apply this structure so that it automatically works for each of the books already defined.
- The data for each portion is stored in bookshelfhelper

BIN
Book_page_mockup.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

BIN
Icon_Quill_BDMurphy_250x250.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

19
MainPage_def.txt Executable file
View File

@@ -0,0 +1,19 @@
This chat is to build a markdown file specifying a template for my book website (BD Bookshelf). You are an expert at UX design and implementing designs in Wordpress which meet wordpress standards strictly. The test/development website I'm using is bdmurphy.me. The main page serves as the readers landing place and a way to navigate to more information. The menu at the top will provide other pages. The main area will be a standard wordpress page showing news/updates and information the user want to show readers. The right sidebar widget shows the authors books listed on goodreads.
There are two helper plugins for this website. The first plugin is "BD Bookshelf", matching the template name. This plugin has items the template needs, the author goodreads ID, Amazon author central ID, utility to scan webpages for author books, and a template transition function. The transition is to help authors move to this teamplate and helper. It will scan the wordpress website and create pages to fit into the theme properly - using the helper plugins.
Future work for the plugin BD Bookshelf is to have a translation function for each blog. The blogs will need to be stored with each translations. If there is no translation for a specific language then English will be shown.
The second plugin is BD BookHelper
This plugin is the respository for book information. Book cover, blurbs, reviews, etc. All stored in five langugaes so the reader can choose. The concept is for the user to have one place to enter they book information and the template will use that information in the various places it is needed, in the language the user specifies. BookHelper also provides a shortcord URL function. The short codes primary function is to redirect readers to pages on seller sites to enter reviews. The short URL redirect can be used by the author for any feature.
The main menu should have the following as the default - the user can use Wordpress to change these.
- Website - this should be the website URL name with no "." extention. bdmurphy not bdmurphy.me.
- My books - a page listing the authors books using a scheme similar to Amazon detail pages. - defined seperately. My books will have submenu items for each book. When the user hovers over the menu item "My Books" a list of the books will popup. The list of books comes from the BookHelper plugin. On the page there is a list of the books. The user can select any of the books by cover image or title and go to a specific detail page for that book.
- Reviews - A page with the listing summary of reviews - the reviews are put into BD BookHelper.
- Blogs - this is a normal blog page, listing blog posts the author created.
- About - A page with a submenu to show 1) Author information (pic, blurb, awards, social media links) 2) Privacy page information. 3) signup to receive email updates - to be built with another plugin. 4) Past emails, in a list similar to the blog.
On the far right of the menu bar is a language dropdown selector. The languages to support are English, French, German, Spanish, Russian, and simplified Chinese. All test/menu items will need translated version to show up.
Generate a markdown file that can be used to create this template. To start assume the helper plugins exist, but any specific features the template will need should be provided at the bottom of the markdown file under the plugin name section.

112
MainPage_spec.md Executable file
View File

@@ -0,0 +1,112 @@
This specification is designed to serve as a high-level visual and functional blueprint for building the **BD Bookshelf** WordPress theme. It outlines the structural hierarchy and the specific data requirements for the developer or AI to implement.
global prompt. You are an experience UX designer that creates wordpress templates which strictly follow wordpress standards and best practices.
# BD Bookshelf: Theme Specification & Design Blueprint
## 1. Global Visual Layout
The layout follows a traditional professional author structure with a focus on scannability and accessibility.
| Section | Description |
| --- | --- |
| **Header (Left)** | **Site Identity**: Displays the domain name without the extension (e.g., "bdmurphy").
|
| **Header (Center)** | **Primary Navigation**: Horizontal menu with dynamic dropdowns for books and about sections.
|
| **Header (Right)** | **Language Selector**: Global dropdown for English, French, German, Spanish, Russian, and Simplified Chinese.
|
| **Main Content** | **Activity Stream**: A standard WordPress loop displaying news, updates, and general site content.
|
| **Sidebar (Right)** | **Goodreads Widget**: A vertical list of the author’s books pulled from the Goodreads API.
|
---
## 2. Navigation & User Experience (UX)
The navigation bar acts as the primary interface for content discovery.
* **Website Home**: Linked to the root URL; the label is dynamically generated from the site name (stripping `.me`).
* **My Books (Dropdown)**:
* **Main Page**: A grid layout similar to Amazon detail pages showing book covers and titles.
* **Dynamic Submenu**: Hovering over "My Books" displays a list of individual book titles fetched from the **BD BookHelper** plugin.
* **Reviews**: A dedicated landing page aggregating review summaries stored in the helper plugin.
* **Blogs**: Standard WordPress post list.
* **About (Dropdown)**:
1. **Author Profile**: Bio, photo, awards, and social media links.
2. **Privacy**: Legal information page.
3. **Newsletter**: Email signup form integration.
4. **Archives**: A list of previously sent email updates.
---
## 3. Multilingual Infrastructure
The theme must respond to the **Language Selector** to provide a localized experience.
* **Content Fallback**: If a translation for a blog post or book detail is unavailable in the selected language, the system must default to displaying the English version.
* **Stored Translations**: The **BD BookHelper** plugin acts as the source of truth for book covers, blurbs, and reviews in all five supported non-English languages.
---
## 4. Developer Requirements: Plugin Integration
### BD Bookshelf (Theme Utility Plugin)
* **Metadata Storage**: Must hold the Author Goodreads ID and Amazon Author Central ID.
* **Migration Tools**: Includes a scanner to identify existing book pages and a transition function to map them into the new template format.
* **Translation Management**: Handles the "English-as-fallback" logic for the blog engine.
### BD BookHelper (Content Repository Plugin)
* **Unified Entry**: Provides one interface for entering book covers, blurbs, and reviews in multiple languages.
* **Shortcodes**:
* Provides redirect URLs for seller sites (e.g., Amazon review pages).
* Allows the author to create custom short-URL redirects for any feature.
* **Data Provider**: Exposes book lists and details to the theme for the "My Books" navigation and detail pages.

Binary file not shown.

Binary file not shown.

BIN
Old/BDMurphy_production_build.zip Executable file

Binary file not shown.

BIN
Old/bd-bookshelfhelper.zip Executable file

Binary file not shown.

View File

@@ -0,0 +1,32 @@
<?php
/**
* Plugin Name: BD BookShelfHelper
* Plugin URI: https://authorbdmurphy.com
* Description: Manages book data, series, pen names, short links, reviews, and ratings for the BD Murphy Bookshelf theme.
* Version: 1.5.0
* Author: B.D. Murphy
* Author URI: https://authorbdmurphy.com
* License: GPL v2 or later
* Text Domain: bd-bookshelfhelper
*/
if (!defined('ABSPATH')) {
exit;
}
define('BD_BHS_VERSION', '1.5.0');
define('BD_BHS_DIR', plugin_dir_path(__FILE__));
define('BD_BHS_URI', plugin_dir_url(__FILE__));
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';
function BD_BookShelfHelper() {
return BD_BookShelfHelper::instance();
}
add_action('plugins_loaded', 'BD_BookShelfHelper');
register_activation_hook(__FILE__, array('BD_BookShelfHelper_Install', 'activate'));
register_deactivation_hook(__FILE__, array('BD_BookShelfHelper_Install', 'deactivate'));

View File

@@ -0,0 +1,480 @@
<?php
/**
* Admin Class
*/
if (!defined('ABSPATH')) {
exit;
}
class BD_BookShelfHelper_Admin {
private static $instance = null;
public static function instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
add_action('admin_menu', array($this, 'add_menu_pages'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_assets'));
add_action('add_meta_boxes', array($this, 'add_meta_boxes'));
add_action('save_post', array($this, 'save_meta_boxes'));
add_action('wp_ajax_bdm_update_links', array($this, 'ajax_update_links'));
}
public function add_menu_pages() {
add_menu_page(
__('BD BookHelper', 'bd-bookshelfhelper'),
__('BD BookHelper', 'bd-bookshelfhelper'),
'manage_options',
'bd-bookshelf',
'',
'dashicons-book',
30
);
add_submenu_page(
'bd-bookshelf',
__('Books', 'bd-bookshelfhelper'),
__('All Books', 'bd-bookshelfhelper'),
'manage_options',
'edit.php?post_type=book'
);
add_submenu_page(
'bd-bookshelf',
__('Add Book', 'bd-bookshelfhelper'),
__('Add Book', 'bd-bookshelfhelper'),
'manage_options',
'post-new.php?post_type=book'
);
add_submenu_page(
'bd-bookshelf',
__('Series', 'bd-bookshelfhelper'),
__('Series', 'bd-bookshelfhelper'),
'manage_options',
'edit-tags.php?taxonomy=series&post_type=book'
);
add_submenu_page(
'bd-bookshelf',
__('Pen Names', 'bd-bookshelfhelper'),
__('Pen Names', 'bd-bookshelfhelper'),
'manage_options',
'bd-bookshelf-pen-names',
array($this, 'pen_names_page')
);
add_submenu_page(
'bd-bookshelf',
__('Settings', 'bd-bookshelfhelper'),
__('Settings', 'bd-bookshelfhelper'),
'manage_options',
'bd-bookshelf-settings',
array($this, 'settings_page')
);
}
public function enqueue_assets($hook) {
$screen = get_current_screen();
if ($screen->post_type === 'book' || $screen->taxonomy === 'series') {
wp_enqueue_media();
}
}
public function add_meta_boxes() {
add_meta_box('bd_book_details', __('Book Details', array($this, 'book_details_meta_box'), 'book', 'normal', 'high');
add_meta_box('bd_book_seo', __('SEO Settings', array($this, 'book_seo_meta_box'), 'book', 'side', 'low');
}
public function book_details_meta_box($post) {
$languages = BD_BookShelfHelper::get_supported_languages();
wp_nonce_field('bd_book_meta', 'bd_book_nonce');
echo '<div class="bd-book-meta-box">';
echo '<h3>' . esc_html__('Core Fields', 'bd-bookshelfhelper') . '</h3>';
echo '<p><label>' . esc_html__('Pen Name', 'bd-bookshelfhelper') . '</label>';
echo '<select name="pen_name_id">';
echo '<option value="">' . esc_html__('B.D. Murphy', 'bd-bookshelfhelper') . '</option>';
global $wpdb;
$table_name = $wpdb->prefix . 'bd_pen_names';
$pen_names = $wpdb->get_results("SELECT id, pen_name FROM {$table_name} WHERE active = 1 ORDER BY pen_name");
$pen_name_id = get_post_meta($post->ID, 'pen_name_id', true);
if ($pen_names) {
foreach ($pen_names as $pn) {
echo '<option value="' . esc_attr($pn->id) . '"' . selected($pen_name_id, $pn->id, false) . '>' . esc_html($pn->pen_name) . '</option>';
}
}
echo '</select></p>';
echo '<p><label>' . esc_html__('ASIN (Amazon)', 'bd-bookshelfhelper') . '</label>';
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 '<input type="text" name="isbn" value="' . esc_attr(get_post_meta($post->ID, 'isbn', true)) . '" class="regular-text"></p>';
echo '<p><label>' . esc_html__('Short Link Slug', 'bd-bookshelfhelper') . '</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 '<p class="description">' . esc_html__('Auto-generated if empty', 'bd-bookshelfhelper') . '</p></p>';
$series_terms = get_terms(array('taxonomy' => 'series', 'hide_empty' => false));
if ($series_terms) {
echo '<p><label>' . esc_html__('Series', 'bd-bookshelfhelper') . '</label>';
echo '<select name="series_id">';
echo '<option value="">' . esc_html__('None / Standalone', 'bd-bookshelfhelper') . '</option>';
$current_series = get_the_terms($post->ID, 'series');
$current_series_id = $current_series && !is_wp_error($current_series) ? $current_series[0]->term_id : 0;
foreach ($series_terms as $term) {
echo '<option value="' . esc_attr($term->term_id) . '"' . selected($current_series_id, $term->term_id, false) . '>' . esc_html($term->name) . '</option>';
}
echo '</select></p>';
if ($current_series_id) {
echo '<p><label>' . esc_html__('Volume Number', 'bd-bookshelfhelper') . '</label>';
echo '<input type="number" name="volume_number" value="' . esc_attr(get_post_meta($post->ID, 'volume_number', true)) . '" min="1"></p>';
}
}
echo '<h3>' . esc_html__('Language Content', 'bd-bookshelfhelper') . '</h3>';
foreach ($languages as $code => $name) {
echo '<fieldset class="bd-lang-fieldset">';
echo '<legend>' . esc_html($name) . ' (' . strtoupper($code) . ')</legend>';
echo '<p><label>' . esc_html__('Title', 'bd-bookshelfhelper') . '</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 '<p><label>' . esc_html__('Back Cover Blurb', 'bd-bookshelfhelper') . '</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 '<p><label>' . esc_html__('Preview Excerpt', 'bd-bookshelfhelper') . '</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 '<p><label>' . esc_html__('Pull Quotes', 'bd-bookshelfhelper') . ' (1-5)</label>';
for ($i = 1; $i <= 5; $i++) {
echo '<input type="text" name="pull_quote_' . esc_attr($code) . '_' . $i . '" value="' . esc_attr(get_post_meta($post->ID, 'pull_quote_' . $code . '_' . $i, true)) . '" class="large-text" placeholder="' . esc_attr__('Quote ' . $i, 'bd-bookshelfhelper') . '">';
}
echo '</p>';
echo '<p><label>' . esc_html__('Cover Image', 'bd-bookshelfhelper') . '</label>';
$cover_id = get_post_meta($post->ID, 'cover_' . $code, true);
echo '<input type="hidden" name="cover_' . esc_attr($code) . '" value="' . esc_attr($cover_id) . '">';
echo '<button class="button bd-cover-upload" data-target="cover_' . esc_attr($code) . '">' . esc_html__('Upload Cover', 'bd-bookshelfhelper') . '</button>';
if ($cover_id) {
echo wp_get_attachment_image($cover_id, 'thumbnail');
}
echo '</p>';
echo '</fieldset>';
}
echo '<h3>' . esc_html__('Retailer Links', 'bd-bookshelfhelper') . '</h3>';
echo '<p><label>' . esc_html__('Amazon (Ebook)', 'bd-bookshelfhelper') . '</label>';
echo '<input type="url" name="ebook_url_amazon" value="' . esc_attr(get_post_meta($post->ID, 'ebook_url_amazon', true)) . '" class="large-text"></p>';
echo '<p><label>' . esc_html__('Apple Books', 'bd-bookshelfhelper') . '</label>';
echo '<input type="url" name="ebook_url_apple" value="' . esc_attr(get_post_meta($post->ID, 'ebook_url_apple', true)) . '" class="large-text"></p>';
echo '<p><label>' . esc_html__('Google Play', 'bd-bookshelfhelper') . '</label>';
echo '<input type="url" name="ebook_url_google" value="' . esc_attr(get_post_meta($post->ID, 'ebook_url_google', true)) . '" class="large-text"></p>';
echo '<p><label>' . esc_html__('Barnes & Noble (Ebook)', 'bd-bookshelfhelper') . '</label>';
echo '<input type="url" name="ebook_url_barnes" value="' . esc_attr(get_post_meta($post->ID, 'ebook_url_barnes', true)) . '" class="large-text"></p>';
echo '<p><label>' . esc_html__('Kobo', 'bd-bookshelfhelper') . '</label>';
echo '<input type="url" name="ebook_url_kobo" value="' . esc_attr(get_post_meta($post->ID, 'ebook_url_kobo', true)) . '" class="large-text"></p>';
echo '<p><label>' . esc_html__('Barnes & Noble (Paperback)', 'bd-bookshelfhelper') . '</label>';
echo '<input type="url" name="paperback_url_barnes" value="' . esc_attr(get_post_meta($post->ID, 'paperback_url_barnes', true)) . '" class="large-text"></p>';
echo '<p><label>' . esc_html__('Barnes & Noble (Hardback)', 'bd-bookshelfhelper') . '</label>';
echo '<input type="url" name="hardback_url_barnes" value="' . esc_attr(get_post_meta($post->ID, 'hardback_url_barnes', true)) . '" class="large-text"></p>';
echo '<h3>' . esc_html__('Ratings', 'bd-bookshelfhelper') . '</h3>';
echo '<p><label>' . esc_html__('Amazon Rating', 'bd-bookshelfhelper') . '</label>';
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 '<input type="text" name="goodreads_rating" value="' . esc_attr(get_post_meta($post->ID, 'goodreads_rating', true)) . '" placeholder="4.2"></p>';
echo '</div>';
}
public function book_seo_meta_box($post) {
echo '<p><label>' . esc_html__('SEO Title', 'bd-bookshelfhelper') . '</label>';
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 '<textarea name="seo_description" rows="4" class="large-text">' . esc_textarea(get_post_meta($post->ID, 'seo_description', true)) . '</textarea></p>';
}
public function save_meta_boxes($post_id) {
if (!isset($_POST['bd_book_nonce']) || !wp_verify_nonce($_POST['bd_book_nonce'], 'bd_book_meta')) {
return $post_id;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return $post_id;
}
if (!current_user_can('edit_post', $post_id)) {
return $post_id;
}
$fields = array(
'pen_name_id', 'asin', 'isbn', 'short_link_slug', 'volume_number',
'amazon_rating', 'goodreads_rating', 'seo_title', 'seo_description'
);
foreach ($fields as $field) {
if (isset($_POST[$field])) {
update_post_meta($post_id, $field, sanitize_text_field($_POST[$field]));
}
}
$languages = BD_BookShelfHelper::get_supported_languages();
foreach ($languages as $code => $name) {
$title_key = 'title_' . $code;
$blurb_key = 'blurb_' . $code;
$preview_key = 'preview_excerpt_' . $code;
$cover_key = 'cover_' . $code;
if (isset($_POST[$title_key])) {
update_post_meta($post_id, $title_key, sanitize_text_field($_POST[$title_key]));
}
if (isset($_POST[$blurb_key])) {
update_post_meta($post_id, $blurb_key, sanitize_textarea_field($_POST[$blurb_key]));
}
if (isset($_POST[$preview_key])) {
update_post_meta($post_id, $preview_key, sanitize_textarea_field($_POST[$preview_key]));
}
if (isset($_POST[$cover_key])) {
update_post_meta($post_id, $cover_key, absint($_POST[$cover_key]));
}
for ($i = 1; $i <= 5; $i++) {
$quote_key = 'pull_quote_' . $code . '_' . $i;
if (isset($_POST[$quote_key])) {
update_post_meta($post_id, $quote_key, sanitize_text_field($_POST[$quote_key]));
}
}
}
$retailer_fields = array(
'ebook_url_amazon', 'ebook_url_apple', 'ebook_url_google',
'ebook_url_barnes', 'ebook_url_kobo',
'paperback_url_barnes', 'hardback_url_barnes'
);
foreach ($retailer_fields as $field) {
if (isset($_POST[$field])) {
update_post_meta($post_id, $field, esc_url_raw($_POST[$field]));
}
}
if (isset($_POST['series_id'])) {
$series_id = absint($_POST['series_id']);
if ($series_id > 0) {
wp_set_post_terms($post_id, array($series_id), 'series');
} else {
wp_set_post_terms($post_id, array(), 'series');
}
}
if (empty($_POST['short_link_slug'])) {
$title = get_the_title($post_id);
$slug = $this->generate_short_slug($title);
update_post_meta($post_id, 'short_link_slug', $slug);
}
}
public function generate_short_slug($title) {
$title = preg_replace('/[^a-zA-Z0-9\s]/', '', $title);
$words = explode(' ', $title);
$stop_words = array('the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for');
$significant_words = array_filter($words, function($word) use ($stop_words) {
return !in_array(strtolower($word), $stop_words) && strlen($word) > 2;
});
$slug = '';
foreach ($significant_words as $word) {
$slug .= strtolower(substr($word, 0, 1));
}
if (strlen($slug) < 2) {
$slug = substr(implode('', $significant_words), 0, 4);
}
global $wpdb;
$original_slug = $slug;
$counter = 0;
while (true) {
$exists = $wpdb->get_var($wpdb->prepare(
"SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = 'short_link_slug' AND meta_value = %s",
$slug
));
if (!$exists) {
break;
}
$counter++;
$slug = $original_slug . $counter;
}
return $slug;
}
public function pen_names_page() {
global $wpdb;
$table_name = $wpdb->prefix . 'bd_pen_names';
if (isset($_POST['bd_add_pen_name']) && check_admin_referer('bd_add_pen_name')) {
$pen_name = sanitize_text_field($_POST['new_pen_name']);
$notes = sanitize_textarea_field($_POST['new_notes']);
if ($pen_name) {
$wpdb->insert($table_name, array(
'pen_name' => $pen_name,
'notes' => $notes,
'active' => 1,
'created_at' => current_time('mysql'),
));
echo '<div class="updated"><p>' . esc_html__('Pen name added.', 'bd-bookshelfhelper') . '</p></div>';
}
}
if (isset($_POST['bd_update_pen_status'])) {
$id = absint($_POST['pen_name_id']);
$active = isset($_POST['pen_active']) ? 1 : 0;
$wpdb->update($table_name, array('active' => $active), array('id' => $id));
}
if (isset($_POST['bd_delete_pen_name'])) {
$id = absint($_POST['pen_name_id']);
$wpdb->delete($table_name, array('id' => $id));
}
$pen_names = $wpdb->get_results("SELECT * FROM {$table_name} ORDER BY pen_name");
echo '<div class="wrap">';
echo '<h1>' . esc_html__('Pen Names', 'bd-bookshelfhelper') . '</h1>';
echo '<form method="post">';
wp_nonce_field('bd_add_pen_name');
echo '<table class="form-table">';
echo '<tr><th>' . esc_html__('Add New Pen Name', 'bd-bookshelfhelper') . '</th>';
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 '<tr><th>' . esc_html__('Notes', 'bd-bookshelfhelper') . '</th>';
echo '<td><textarea name="new_notes" rows="3" class="regular-text"></textarea></td></tr>';
echo '</table>';
submit_button(__('Add Pen Name', 'bd-bookshelfhelper'), 'primary', 'bd_add_pen_name');
echo '</form>';
echo '<h2>' . esc_html__('Existing Pen Names', 'bd-bookshelfhelper') . '</h2>';
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 '<tbody>';
if ($pen_names) {
foreach ($pen_names as $pn) {
echo '<tr>';
echo '<td>' . esc_html($pn->id) . '</td>';
echo '<td>' . esc_html($pn->pen_name) . '</td>';
echo '<td>' . esc_html($pn->notes) . '</td>';
echo '<td>' . ($pn->active ? esc_html__('Active', 'bd-bookshelfhelper') : esc_html__('Inactive', 'bd-bookshelfhelper')) . '</td>';
echo '<td>';
echo '<form method="post" style="display:inline;">';
wp_nonce_field('bd_update_pen_status');
echo '<input type="hidden" name="pen_name_id" value="' . esc_attr($pn->id) . '">';
echo '<input type="checkbox" name="pen_active" value="1"' . checked($pn->active, 1, false) . '>';
submit_button(__('Update', 'bd-bookshelfhelper'), 'secondary', 'bd_update_pen_status', false);
echo '</form> ';
echo '<form method="post" style="display:inline;">';
wp_nonce_field('bd_delete_pen_name');
echo '<input type="hidden" name="pen_name_id" value="' . esc_attr($pn->id) . '">';
submit_button(__('Delete', 'bd-bookshelfhelper'), 'delete', 'bd_delete_pen_name', false, array('onclick' => 'return confirm("Delete this pen name?");'));
echo '</form>';
echo '</td>';
echo '</tr>';
}
}
echo '</tbody></table></div>';
}
public function settings_page() {
if (isset($_POST['bd_save_settings'])) {
$affiliates = array(
'amazon' => sanitize_text_field($_POST['affiliate_amazon']),
'apple' => sanitize_text_field($_POST['affiliate_apple']),
'barnes' => sanitize_text_field($_POST['affiliate_barnes']),
'kobo' => sanitize_text_field($_POST['affiliate_kobo']),
'google' => sanitize_text_field($_POST['affiliate_google']),
);
update_option('bdm_affiliate_codes', $affiliates);
$goodreads_id = sanitize_text_field($_POST['goodreads_author_id']);
update_option('bdm_goodreads_author_id', $goodreads_id);
echo '<div class="updated"><p>' . esc_html__('Settings saved.', 'bd-bookshelfhelper') . '</p></div>';
}
$affiliates = get_option('bdm_affiliate_codes', array());
$goodreads_id = get_option('bdm_goodreads_author_id', '30469651');
echo '<div class="wrap">';
echo '<h1>' . esc_html__('BD BookHelper Settings', 'bd-bookshelfhelper') . '</h1>';
echo '<form method="post">';
echo '<h2>' . esc_html__('Affiliate Codes', 'bd-bookshelfhelper') . '</h2>';
echo '<p>' . esc_html__('Enter your affiliate IDs for each retailer. Links include these IDs when available.', 'bd-bookshelfhelper') . '</p>';
echo '<table class="form-table">';
echo '<tr><th>Amazon</th><td><input type="text" name="affiliate_amazon" value="' . esc_attr(isset($affiliates['amazon']) ? $affiliates['amazon'] : '') . '" class="regular-text"></td></tr>';
echo '<tr><th>Apple Books</th><td><input type="text" name="affiliate_apple" value="' . esc_attr(isset($affiliates['apple']) ? $affiliates['apple'] : '') . '" class="regular-text"></td></tr>';
echo '<tr><th>Barnes & Noble</th><td><input type="text" name="affiliate_barnes" value="' . esc_attr(isset($affiliates['barnes']) ? $affiliates['barnes'] : '') . '" class="regular-text"></td></tr>';
echo '<tr><th>Kobo</th><td><input type="text" name="affiliate_kobo" value="' . esc_attr(isset($affiliates['kobo']) ? $affiliates['kobo'] : '') . '" class="regular-text"></td></tr>';
echo '<tr><th>Google Play</th><td><input type="text" name="affiliate_google" value="' . esc_attr(isset($affiliates['google']) ? $affiliates['google'] : '') . '" class="regular-text"></td></tr>';
echo '</table>';
echo '<h2>' . esc_html__('Goodreads Integration', 'bd-bookshelfhelper') . '</h2>';
echo '<p><label>' . esc_html__('Goodreads Author ID', 'bd-bookshelfhelper') . '</label>';
echo '<input type="text" name="goodreads_author_id" value="' . esc_attr($goodreads_id) . '" class="regular-text">';
echo '<p class="description">' . esc_html__('Default: 30469651 (B_D_Murphy)', 'bd-bookshelfhelper') . '</p></p>';
submit_button(__('Save Settings', 'bd-bookshelfhelper'), 'primary', 'bd_save_settings');
echo '</form></div>';
}
public function ajax_update_links() {
check_ajax_referer('bd_update_links');
$book_id = absint($_POST['book_id']);
echo json_encode(array('success' => true));
wp_die();
}
}

View File

@@ -0,0 +1,396 @@
<?php
/**
* Main Plugin Class
*/
if (!defined('ABSPATH')) {
exit;
}
class BD_BookShelfHelper {
private static $instance = null;
private $db;
public static function instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->init_hooks();
}
private function init_hooks() {
add_action('init', array($this, 'register_post_types'));
add_action('init', array($this, 'register_taxonomies'));
add_action('init', array($this, 'register_shortlinks'));
add_action('wp_head', array($this, 'output_seo_tags'));
add_filter('nav_menu_items', array($this, 'filter_series_menu_item'), 10, 2);
if (is_admin()) {
BD_BookShelfHelper_Admin::instance();
}
}
public function register_post_types() {
register_post_type('book', array(
'labels' => array(
'name' => __('Books', 'bd-bookshelfhelper'),
'singular_name' => __('Book', 'bd-bookshelfhelper'),
'add_new' => __('Add New', 'bd-bookshelfhelper'),
'add_new_item' => __('Add New Book', 'bd-bookshelfhelper'),
'edit_item' => __('Edit Book', 'bd-bookshelfhelper'),
'new_item' => __('New Book', 'bd-bookshelfhelper'),
'view_item' => __('View Book', 'bd-bookshelfhelper'),
'search_items' => __('Search Books', 'bd-bookshelfhelper'),
),
'public' => true,
'has_archive' => true,
'show_in_menu' => 'bd-bookshelf',
'supports' => array('title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'),
'rewrite' => array('slug' => 'book'),
'menu_icon' => 'dashicons-book',
'show_in_rest' => true,
));
}
public function register_taxonomies() {
register_taxonomy('series', 'book', array(
'labels' => array(
'name' => __('Series', 'bd-bookshelfhelper'),
'singular_name' => __('Series', 'bd-bookshelfhelper'),
'add_new_item' => __('Add New Series', 'bd-bookshelfhelper'),
'new_item_name' => __('New Series Name', 'bd-bookshelfhelper'),
),
'hierarchical' => true,
'public' => true,
'show_admin_column' => true,
'rewrite' => array('slug' => 'series'),
'show_in_rest' => true,
));
}
public function register_shortlinks() {
add_rewrite_rule('^([a-z0-9-]+)/?$', 'index.php?book_slug=$matches[1]', 'top');
add_rewrite_rule('^([a-z0-9-]+)r([a-z])$', 'index.php?book_slug=$matches[1]&review=$matches[2]', 'top');
}
public function filter_series_menu_item($items, $args) {
if ($args->theme_location !== 'primary') {
return $items;
}
$series_count = self::series_count();
if ($series_count > 0) {
$series_item = '<li class="menu-item series-item"><a href="' . home_url('/series/') . '">' . __('Series', 'bd-bookshelfhelper') . '</a></li>';
if (preg_match('/(Books<li.*?<\/li>)/s', $items, $matches)) {
$items = str_replace($matches[1], $matches[1] . $series_item, $items);
}
}
return $items;
}
public function output_seo_tags() {
if (!is_singular('book')) {
return;
}
$book_id = get_the_ID();
$title = get_post_meta($book_id, 'seo_title', true);
if (!$title) {
$title = get_the_title($book_id) . ' | ' . get_bloginfo('name');
}
$description = get_post_meta($book_id, 'seo_description', true);
if (!$description) {
$excerpt = get_post_meta($book_id, 'blurb_excerpt', true);
$description = substr($excerpt, 0, 160);
}
echo '<meta name="description" content="' . esc_attr($description) . '">' . "\n";
echo '<meta property="og:title" content="' . esc_attr($title) . '">' . "\n";
echo '<meta property="og:description" content="' . esc_attr($description) . '">' . "\n";
echo '<meta property="og:type" content="book">' . "\n";
echo '<meta name="twitter:card" content="summary_large_image">' . "\n";
$schema = self::generate_schema($book_id);
echo '<script type="application/ld+json">' . json_encode($schema) . '</script>' . "\n";
}
public function generate_schema($book_id) {
$book = get_post($book_id);
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'Book',
'name' => $book->post_title,
'author' => array(
'@type' => 'Person',
'name' => get_bloginfo('name'),
),
);
$asin = get_post_meta($book_id, 'asin', true);
if ($asin) {
$schema['isbn'] = $asin;
}
$cover_id = get_post_thumbnail_id($book_id);
if ($cover_id) {
$cover_url = wp_get_attachment_url($cover_id);
$schema['image'] = $cover_url;
}
$description = get_post_meta($book_id, 'blurb', true);
if ($description) {
$schema['description'] = $description;
}
$series_term = get_the_terms($book_id, 'series');
if ($series_term && !is_wp_error($series_term)) {
$schema['isPartOf'] = array(
'@type' => 'BookSeries',
'name' => $series_term[0]->name,
);
}
return $schema;
}
public static function get_featured_books($limit = 5) {
$args = array(
'post_type' => 'book',
'posts_per_page' => $limit,
'post_status' => 'publish',
'orderby' => 'meta_value',
'meta_key' => 'publish_date',
'order' => 'DESC',
);
$query = new WP_Query($args);
return $query->posts;
}
public static function get_cover_url($book_id, $size = 'thumbnail', $lang = 'en') {
$meta_key = 'cover_' . $lang;
$cover_id = get_post_meta($book_id, $meta_key, true);
if (!$cover_id) {
$cover_id = get_post_thumbnail_id($book_id);
}
if ($cover_id) {
$image = wp_get_attachment_image_url($cover_id, $size);
if ($image) {
return $image;
}
}
return BD_BHS_URI . 'assets/default-cover.png';
}
public static function get_book_url($book_id) {
$slug = get_post_meta($book_id, 'short_link_slug', true);
if ($slug) {
return home_url('/' . $slug);
}
return get_permalink($book_id);
}
public static function get_ratings($book_id) {
$ratings = array();
$amazon_rating = get_post_meta($book_id, 'amazon_rating', true);
if ($amazon_rating) {
$ratings['amazon'] = $amazon_rating;
}
$goodreads_rating = get_post_meta($book_id, 'goodreads_rating', true);
if ($goodreads_rating) {
$ratings['goodreads'] = $goodreads_rating;
}
return $ratings;
}
public static function get_retailer_links($book_id, $format = 'ebook') {
$links = array();
$retailers = array(
'amazon' => array(
'name' => 'Amazon',
'base_url' => 'https://www.amazon.com/dp/',
'param' => '?tag=',
),
'apple' => array(
'name' => 'Apple Books',
'base_url' => 'https://books.apple.com/',
'param' => '?at=',
),
'barnes' => array(
'name' => 'Barnes & Noble',
'base_url' => 'https://www.barnesandnoble.com/',
'param' => '?aid=',
),
'kobo' => array(
'name' => 'Kobo',
'base_url' => 'https://www.kobo.com/',
'param' => '?aid=',
),
'google' => array(
'name' => 'Google Play',
'base_url' => 'https://play.google.com/store/apps/details?id=',
'param' => '&partnerCode=',
),
);
$affiliates = get_option('bdm_affiliate_codes', array());
foreach ($retailers as $key => $retailer) {
$url_meta = $format . '_url_' . $key;
$url = get_post_meta($book_id, $url_meta, true);
if (!$url && $key === 'amazon') {
$asin = get_post_meta($book_id, 'asin', true);
if ($asin) {
$affiliate = isset($affiliates[$key]) ? $affiliates[$key] : '';
$url = $retailer['base_url'] . $asin . $retailer['param'] . $affiliate;
}
}
if ($url) {
$links[] = array(
'name' => $retailer['name'],
'url' => $url,
'retailer' => $key,
);
}
}
return $links;
}
public static function display_retailer_links($book_id, $format = 'ebook') {
$links = self::get_retailer_links($book_id, $format);
foreach ($links as $link) {
echo '<a href="' . esc_url($link['url']) . '" class="retailer-link retailer-' . esc_attr($link['retailer']) . '" target="_blank" rel="noopener noreferrer">' . esc_html($link['name']) . '</a>';
}
}
public static function get_localized_title($book_id, $lang = 'en') {
$title = get_post_meta($book_id, 'title_' . $lang, true);
if (!$title) {
$title = get_the_title($book_id);
}
return $title;
}
public static function get_localized_blurb($book_id, $lang = 'en') {
return get_post_meta($book_id, 'blurb_' . $lang, true);
}
public static function get_blurb_excerpt($book_id, $length = 150) {
$blurb = get_post_meta($book_id, 'blurb', true);
if ($blurb) {
return substr($blurb, 0, $length) . '...';
}
return '';
}
public static function get_preview_excerpt($book_id, $lang = 'en') {
return get_post_meta($book_id, 'preview_excerpt_' . $lang, true);
}
public static function get_pull_quotes($book_id, $lang = 'en') {
$quotes = array();
for ($i = 1; $i <= 5; $i++) {
$quote = get_post_meta($book_id, 'pull_quote_' . $lang . '_' . $i, true);
if ($quote) {
$quotes[] = $quote;
}
}
return $quotes;
}
public static function get_reviews($book_id) {
global $wpdb;
$table_name = $wpdb->prefix . 'bd_reviews';
$reviews = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$table_name} WHERE book_id = %d ORDER BY display_order ASC",
$book_id
));
return $reviews ?: array();
}
public static function get_pen_name($book_id) {
$pen_name_id = get_post_meta($book_id, 'pen_name_id', true);
if ($pen_name_id) {
global $wpdb;
$table_name = $wpdb->prefix . 'bd_pen_names';
$pen_name = $wpdb->get_var($wpdb->prepare(
"SELECT pen_name FROM {$table_name} WHERE id = %d AND active = 1",
$pen_name_id
));
return $pen_name ?: 'B.D. Murphy';
}
return 'B.D. Murphy';
}
public static function get_series_info($book_id) {
$terms = get_the_terms($book_id, 'series');
if ($terms && !is_wp_error($terms)) {
$series = $terms[0];
$volume = get_post_meta($book_id, 'volume_number', true);
return array(
'id' => $series->term_id,
'name' => $series->name,
'slug' => $series->slug,
'volume' => $volume ?: 1,
);
}
return null;
}
public static function series_count() {
$terms = get_terms(array(
'taxonomy' => 'series',
'hide_empty' => true,
'fields' => 'count',
));
return $terms ?: 0;
}
public static function get_supported_languages() {
return array(
'en' => 'English',
'fr' => 'Fran\u00e7ais',
'de' => 'Deutsch',
'es' => 'Espa\u00f1ol',
'it' => 'Italiano',
'pt' => 'Portugu\u00eas',
'ru' => 'Russian',
'ja' => 'Japanese',
'zh' => 'Chinese',
);
}
public static function get_awards() {
return get_option('bdm_awards', array());
}
}

View File

@@ -0,0 +1,167 @@
<?php
/**
* Installation and Database Setup
*/
if (!defined('ABSPATH')) {
exit;
}
class BD_BookShelfHelper_Install {
public static function activate() {
self::create_tables();
self::create_roles();
self::setup_scheduled_tasks();
flush_rewrite_rules();
}
public static function deactivate() {
self::remove_roles();
wp_clear_scheduled_task('bd_bookshelf_goodreads_refresh');
wp_clear_scheduled_task('bd_bookshelf_amazon_scrape');
flush_rewrite_rules();
}
public static function create_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$tables = array(
"CREATE TABLE IF NOT EXISTS {$wpdb->prefix}bd_books (
id bigint(20) NOT NULL AUTO_INCREMENT,
slug varchar(100) NOT NULL,
pen_name_id bigint(20) DEFAULT NULL,
series_id bigint(20) DEFAULT NULL,
volume_number int(11) DEFAULT NULL,
asin varchar(20) DEFAULT NULL,
isbn varchar(20) DEFAULT NULL,
publish_date date DEFAULT NULL,
sort_order int(11) DEFAULT 0,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY slug (slug)
) $charset_collate;",
"CREATE TABLE IF NOT EXISTS {$wpdb->prefix}bd_book_meta (
id bigint(20) NOT NULL AUTO_INCREMENT,
book_id bigint(20) NOT NULL,
language_code varchar(5) NOT NULL,
title varchar(255) DEFAULT NULL,
blurb text,
preview_excerpt text,
PRIMARY KEY (id),
KEY book_id (book_id),
KEY language_code (language_code)
) $charset_collate;",
"CREATE TABLE IF NOT EXISTS {$wpdb->prefix}bd_series (
id bigint(20) NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
slug varchar(100) NOT NULL,
cover_attachment_id bigint(20) DEFAULT NULL,
header_attachment_id bigint(20) DEFAULT NULL,
blurb text,
display_order int(11) DEFAULT 0,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY slug (slug)
) $charset_collate;",
"CREATE TABLE IF NOT EXISTS {$wpdb->prefix}bd_pen_names (
id bigint(20) NOT NULL AUTO_INCREMENT,
pen_name varchar(255) NOT NULL,
notes text,
active tinyint(1) DEFAULT 1,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) $charset_collate;",
"CREATE TABLE IF NOT EXISTS {$wpdb->prefix}bd_reviews (
id bigint(20) NOT NULL AUTO_INCREMENT,
book_id bigint(20) NOT NULL,
comment_text text,
synopsis text,
source_name varchar(255) NOT NULL,
source_url varchar(500) DEFAULT NULL,
display_order int(11) DEFAULT 0,
PRIMARY KEY (id),
KEY book_id (book_id)
) $charset_collate;",
"CREATE TABLE IF NOT EXISTS {$wpdb->prefix}bd_ratings (
id bigint(20) NOT NULL AUTO_INCREMENT,
book_id bigint(20) NOT NULL,
source varchar(20) NOT NULL,
rating decimal(3,2) DEFAULT NULL,
review_count int(11) DEFAULT NULL,
last_updated datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY book_source (book_id, source)
) $charset_collate;",
"CREATE TABLE IF NOT EXISTS {$wpdb->prefix}bd_review_shortlinks (
id bigint(20) NOT NULL AUTO_INCREMENT,
book_id bigint(20) NOT NULL,
site_code varchar(5) NOT NULL,
slug varchar(20) NOT NULL,
destination_url varchar(500) DEFAULT NULL,
status varchar(20) DEFAULT 'pending',
created_at datetime DEFAULT CURRENT_TIMESTAMP,
activated_at datetime DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY slug (slug)
) $charset_collate;",
);
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
foreach ($tables as $table) {
dbDelta($table);
}
self::install_sample_data();
}
public static function install_sample_data() {
global $wpdb;
$pen_names_table = $wpdb->prefix . 'bd_pen_names';
$exists = $wpdb->get_var("SELECT COUNT(*) FROM {$pen_names_table}");
if ($exists == 0) {
$wpdb->insert($pen_names_table, array(
'pen_name' => 'B.D. Murphy',
'notes' => 'Primary author name',
'active' => 1,
'created_at' => current_time('mysql'),
));
}
update_option('bdm_goodreads_author_id', '30469651');
}
public static function create_roles() {
add_role('book_manager', __('Book Manager', 'bd-bookshelfhelper'), array(
'read' => true,
'edit_books' => true,
'edit_others_books' => true,
'publish_books' => true,
'read_private_posts' => true,
));
}
public static function remove_roles() {
remove_role('book_manager');
}
public static function setup_scheduled_tasks() {
if (!wp_next_scheduled('bd_bookshelf_goodreads_refresh')) {
wp_schedule_event(time(), 'daily', 'bd_bookshelf_goodreads_refresh');
}
if (!wp_next_scheduled('bd_bookshelf_amazon_scrape')) {
wp_schedule_event(time(), 'six_hours', 'bd_bookshelf_amazon_scrape');
}
}
}

BIN
Old/bd-murphy-author.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,21 @@
<?php
/**
* 404 Page Template
*/
get_header();
?>
<div class="error-container">
<div class="error-content">
<h1 class="error-code">404</h1>
<h2 class="error-title"><?php esc_html_e('Page Not Found', 'bd-murphy'); ?></h2>
<p class="error-message"><?php esc_html_e('The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.', 'bd-murphy'); ?></p>
<p class="error-actions">
<a href="<?php echo esc_url(home_url('/')); ?>" class="btn-home"><?php esc_html_e('Go Home', 'bd-murphy'); ?></a>
</p>
</div>
</div>
<?php
get_footer();

View File

@@ -0,0 +1,83 @@
<?php
/**
* Template Name: All Books
*/
get_header();
?>
<div class="books-container">
<header class="page-header">
<h1 class="page-title"><?php esc_html_e('Books', 'bd-murphy'); ?></h1>
<?php
if (class_exists('BD_BookShelfHelper') && BD_BookShelfHelper::series_count() > 0) {
echo '<a href="' . home_url('/series/') . '" class="browse-series-link">' . __('Browse by Series', 'bd-murphy') . '</a>';
}
?>
</header>
<div class="books-list">
<?php
if (have_posts()) :
while (have_posts()) :
the_post();
setup_postdata($post);
$book_id = get_the_ID();
$cover_url = BD_BookShelfHelper::get_cover_url($book_id, 'book-cover');
$title = get_the_title();
$author = BD_BookShelfHelper::get_pen_name($book_id);
$ratings = BD_BookShelfHelper::get_ratings($book_id);
$blurb = BD_BookShelfHelper::get_blurb_excerpt($book_id);
$series = BD_BookShelfHelper::get_series_info($book_id);
?>
<article class="book-entry">
<div class="book-entry-cover">
<img src="<?php echo esc_url($cover_url); ?>" alt="<?php echo esc_attr($title); ?>">
</div>
<div class="book-entry-details">
<?php if ($series) : ?>
<span class="book-series-label">
<a href="<?php echo esc_url(home_url('/series/' . $series['slug'])); ?>">
<?php printf(esc_html__('Part of %s', 'bd-murphy'), esc_html($series['name'])); ?>
</a>
</span>
<?php endif; ?>
<h2 class="book-title"><?php echo esc_html($title); ?></h2>
<p class="book-author"><?php echo esc_html($author); ?></p>
<?php if ($ratings) : ?>
<div class="book-ratings">
<?php if (!empty($ratings['amazon'])) : ?>
<span class="rating amazon">Amazon: <?php echo esc_html($ratings['amazon']); ?></span>
<?php endif; ?>
<?php if (!empty($ratings['goodreads'])) : ?>
<span class="rating goodreads">Goodreads: <?php echo esc_html($ratings['goodreads']); ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="book-format-toggle">
<button class="format-btn active" data-format="ebook"><?php esc_html_e('Ebook', 'bd-murphy'); ?></button>
<button class="format-btn" data-format="paperback"><?php esc_html_e('Paperback', 'bd-murphy'); ?></button>
<button class="format-btn" data-format="hardback"><?php esc_html_e('Hardback', 'bd-murphy'); ?></button>
</div>
<div class="book-retailer-links">
<?php BD_BookShelfHelper::display_retailer_links($book_id); ?>
</div>
<p class="book-blurb"><?php echo esc_html($blurb); ?></p>
</div>
</article>
<?php
endwhile;
wp_reset_postdata();
else :
?>
<p class="no-books"><?php esc_html_e('No books available yet.', 'bd-murphy'); ?></p>
<?php endif; ?>
</div>
</div>
<?php
get_footer();

View File

@@ -0,0 +1,46 @@
<?php
/**
* Template Name: Series Listing
*/
get_header();
?>
<div class="series-container">
<header class="page-header">
<h1 class="page-title"><?php esc_html_e('Series', 'bd-murphy'); ?></h1>
</header>
<div class="series-list">
<?php
if (have_posts()) :
while (have_posts()) :
the_post();
$series_id = get_the_ID();
$cover_url = BD_BookShelfHelper::get_series_cover_url($series_id, 'series-card');
$name = get_the_title();
$slug = get_post_field('post_name');
$blurb = BD_BookShelfHelper::get_series_blurb($series_id);
$book_count = BD_BookShelfHelper::get_series_book_count($series_id);
?>
<article class="series-card">
<a href="<?php the_permalink(); ?>">
<img src="<?php echo esc_url($cover_url); ?>" alt="<?php echo esc_attr($name); ?>" class="series-cover">
</a>
<h2 class="series-name">
<a href="<?php the_permalink(); ?>"><?php echo esc_html($name); ?></a>
</h2>
<p class="series-blurb"><?php echo esc_html($blurb); ?></p>
<p class="series-book-count"><?php printf(esc_html(_n('%d book', '%d books', $book_count, 'bd-murphy')), $book_count); ?></p>
</article>
<?php
endwhile;
else :
?>
<p class="no-series"><?php esc_html_e('No series available yet.', 'bd-murphy'); ?></p>
<?php endif; ?>
</div>
</div>
<?php
get_footer();

View File

@@ -0,0 +1,69 @@
<?php
/**
* Template Name: Blog
*/
get_header();
?>
<div class="blog-container">
<header class="page-header">
<h1 class="page-title"><?php single_post_title('', true); ?: esc_html_e('Blog', 'bd-murphy'); ?></h1>
</header>
<div class="blog-list">
<?php
if (have_posts()) :
while (have_posts()) :
the_post();
?>
<article id="post-<?php the_ID(); ?>" <?php post_class('blog-entry'); ?>>
<div class="blog-entry-inner">
<?php if (has_post_thumbnail()) : ?>
<div class="blog-entry-thumbnail">
<a href="<?php the_permalink(); ?>">
<?php the_post_thumbnail('blog-thumb'); ?>
</a>
</div>
<?php endif; ?>
<header class="blog-entry-header">
<h2 class="blog-entry-title">
<a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
</h2>
<div class="blog-entry-meta">
<span class="blog-entry-date"><?php bdm_posted_on(); ?></span>
<?php
$categories = get_the_category();
if (!empty($categories)) {
echo '<span class="blog-entry-category">' . esc_html($categories[0]->name) . '</span>';
}
if (get_theme_mod('show_read_time', true)) {
echo '<span class="blog-entry-readtime">' . bdm_display_reading_time() . '</span>';
}
?>
</div>
</header>
<div class="blog-entry-excerpt">
<?php the_excerpt(); ?>
</div>
</div>
</article>
<?php
endwhile;
the_posts_pagination(array(
'mid_size' => 2,
'prev_text' => __('Previous', 'bd-murphy'),
'next_text' => __('Next', 'bd-murphy'),
));
else :
?>
<p class="no-posts"><?php esc_html_e('No posts found.', 'bd-murphy'); ?></p>
<?php endif; ?>
</div>
</div>
<?php
get_footer();

View File

@@ -0,0 +1,97 @@
/**
* Navigation JavaScript
*/
(function() {
'use strict';
var container, button, menu, links, i, len;
container = document.getElementById('site-nav');
if (!container) {
return;
}
button = container.getElementsByClassName('menu-toggle')[0];
if ('undefined' === typeof button) {
return;
}
menu = container.getElementsByTagName('ul')[0];
if (menu.classList.contains('nav-menu') && window.innerWidth < 768) {
menu.classList.add('nav-menu-hidden');
}
menu.setAttribute('aria-expanded', 'false');
if (-1 === menu.className.indexOf('nav-menu')) {
menu.className += ' nav-menu';
}
button.onclick = function() {
if (-1 !== container.className.indexOf('toggled')) {
container.className = container.className.replace(' toggled', '');
button.setAttribute('aria-expanded', 'false');
menu.setAttribute('aria-expanded', 'false');
menu.classList.add('nav-menu-hidden');
} else {
container.className += ' toggled';
button.setAttribute('aria-expanded', 'true');
menu.setAttribute('aria-expanded', 'true');
menu.classList.remove('nav-menu-hidden');
}
};
links = menu.getElementsByTagName('a');
for (i = 0, len = links.length; i < len; i++) {
links[i].addEventListener('focus', toggleFocus, true);
links[i].addEventListener('blur', toggleFocus, true);
}
function toggleFocus() {
var self = this;
while (-1 === self.className.indexOf('menu-item')) {
if ('undefined' === typeof self.parentElement) {
break;
}
self = self.parentElement;
}
if (self.classList.contains('current-menu-item')) {
return;
}
var dropdown = self.querySelector('.sub-menu');
if (dropdown) {
dropdown.classList.toggle('focus');
}
var siblings = self.parentElement.children;
for (i = 0; i < siblings.length; i++) {
if (siblings[i] !== self) {
siblings[i].classList.remove('focus');
}
}
self.classList.toggle('focus');
}
var supportsTouch = 'ontouchstart' in window;
if (supportsTouch) {
menu.addEventListener('touchstart', function(e) {
if (e.touches.length > 1) {
return true;
}
}, { passive: true });
}
document.addEventListener('scroll', function() {
var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
var banner = document.querySelector('.header-banner');
if (banner && scrollTop > banner.offsetHeight) {
container.classList.add('scrolled');
} else {
container.classList.remove('scrolled');
}
}, { passive: true });
})();

View File

@@ -0,0 +1,109 @@
/**
* Social Share JavaScript
*/
(function() {
'use strict';
var shareContainer = document.querySelector('.social-share-buttons');
if (!shareContainer) {
return;
}
if (navigator.share) {
var webShareBtns = shareContainer.querySelectorAll('.share-btn[data-action="webshare"]');
webShareBtns.forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.preventDefault();
var url = btn.dataset.url;
var title = btn.dataset.title;
navigator.share({
title: title,
url: url
}).catch(function(err) {
console.log('Error sharing:', err);
});
});
});
}
var copyBtn = shareContainer.querySelector('.share-copy');
if (copyBtn) {
copyBtn.addEventListener('click', function() {
var url = copyBtn.dataset.url;
navigator.clipboard.writeText(url).then(function() {
showToast('Link copied to clipboard!');
}).catch(function(err) {
var textArea = document.createElement('textarea');
textArea.value = url;
textArea.style.position = 'fixed';
textArea.style.left = '-9999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
showToast('Link copied to clipboard!');
} catch (err) {
showToast('Failed to copy link');
}
document.body.removeChild(textArea);
});
});
}
function showToast(message) {
var toast = document.createElement('div');
toast.className = 'share-toast';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(function() {
toast.classList.add('show');
}, 10);
setTimeout(function() {
toast.classList.remove('show');
setTimeout(function() {
document.body.removeChild(toast);
}, 300);
}, 2000);
}
var shareBtn = document.querySelector('.share-btn:not(.share-copy)');
if (shareBtn) {
shareBtn.addEventListener('click', function() {
var btn = this;
var url = btn.href;
if (navigator.share) {
navigator.share({
title: document.title,
url: window.location.href
}).catch(function() {});
}
});
}
var formatBtns = document.querySelectorAll('.format-btn');
formatBtns.forEach(function(btn) {
btn.addEventListener('click', function() {
var format = btn.dataset.format;
var container = btn.closest('.book-entry, .book-page-content');
container.querySelectorAll('.format-btn').forEach(function(b) {
b.classList.remove('active');
});
btn.classList.add('active');
var retailerLinks = container.querySelectorAll('.book-retailer-links .retailer-link');
retailerLinks.forEach(function(link) {
if (link.dataset.format === format || link.dataset.formats && link.dataset.formats.includes(format)) {
link.style.display = '';
} else {
link.style.display = 'none';
}
});
});
});
})();

View File

@@ -0,0 +1,847 @@
/**
* BD Murphy Bookshelf Styles
*/
:root {
--bdm-navy: #1B3A6B;
--bdm-slate: #2E6DA4;
--bdm-sky: #4A90D9;
--bdm-gold: #C8A96E;
--bdm-cream: #F5F0E8;
--bdm-parchment: #EDE4D0;
--bdm-body-text: #2C2C2C;
--bdm-mid-grey: #555555;
--bdm-light-grey: #888888;
--bdm-active-green: #2D6A4F;
--bdm-heading-font: 'Playfair Display', Georgia, serif;
--bdm-body-font: 'Lato', Calibri, sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: var(--bdm-body-font);
font-size: 16px;
line-height: 1.6;
color: var(--bdm-body-text);
background-color: var(--bdm-cream);
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--bdm-heading-font);
font-weight: 600;
line-height: 1.3;
margin: 0 0 0.5em;
}
h1 { font-size: 2.5rem; }
h2 { font-size: 2rem; }
h3 { font-size: 1.5rem; }
h4 { font-size: 1.25rem; }
a {
color: var(--bdm-slate);
text-decoration: none;
transition: color 0.2s ease;
}
a:hover {
color: var(--bdm-gold);
}
img {
max-width: 100%;
height: auto;
}
.site-header {
position: relative;
}
.header-banner {
width: 100%;
max-height: 200px;
overflow: hidden;
}
.header-banner img {
width: 100%;
height: 200px;
object-fit: cover;
}
.site-nav {
background-color: var(--bdm-navy);
position: sticky;
top: 0;
z-index: 100;
}
.site-nav.scrolled {
border-top: 2px solid var(--bdm-gold);
}
.nav-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.nav-menu-container ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
}
.nav-menu-container li {
margin: 0;
}
.nav-menu-container a {
display: block;
padding: 15px 20px;
color: #fff;
font-family: var(--bdm-body-font);
font-weight: 500;
}
.nav-menu-container a:hover,
.nav-menu-container .current-menu-item a {
color: var(--bdm-gold);
border-bottom: 2px solid var(--bdm-gold);
}
.menu-toggle {
display: none;
background: none;
border: none;
color: #fff;
padding: 15px;
cursor: pointer;
}
.hamburger {
display: block;
width: 24px;
height: 2px;
background: #fff;
position: relative;
}
.hamburger::before,
.hamburger::after {
content: '';
position: absolute;
width: 100%;
height: 2px;
background: #fff;
left: 0;
}
.hamburger::before { top: -8px; }
.hamburger::after { top: 8px; }
.site-footer {
background-color: var(--bdm-navy);
color: #fff;
padding: 40px 20px;
}
.footer-container {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 30px;
}
.footer-zone h4 {
color: var(--bdm-gold);
margin-bottom: 15px;
}
.social-link svg {
width: 24px;
height: 24px;
fill: #fff;
margin-right: 10px;
transition: fill 0.2s ease;
}
.social-link:hover svg {
fill: var(--bdm-gold);
}
.donate-btn {
display: inline-block;
padding: 10px 20px;
background-color: var(--bdm-gold);
color: var(--bdm-navy);
border-radius: 4px;
margin-right: 10px;
margin-bottom: 10px;
}
.donate-btn:hover {
background-color: #fff;
color: var(--bdm-navy);
}
.footer-links ul {
list-style: none;
margin: 0;
padding: 0;
}
.footer-links a {
color: #fff;
display: block;
padding: 5px 0;
}
.footer-links a:hover {
color: var(--bdm-gold);
}
.footer-signup-prompt {
margin-top: 15px;
}
.footer-signup-prompt a {
color: var(--bdm-gold);
text-decoration: underline;
}
.footer-bottom {
max-width: 1200px;
margin: 30px auto 0;
padding-top: 20px;
border-top: 1px solid rgba(255,255,255,0.2);
text-align: center;
font-size: 0.875rem;
}
.home-container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
display: grid;
grid-template-columns: 66% 34%;
gap: 30px;
}
.home-left-column .home-content {
background-color: #fff;
padding: 30px;
border-radius: 8px;
}
.home-right-column {
padding-top: 0;
}
.book-shelf-widget {
display: flex;
flex-direction: column;
gap: 20px;
}
.book-shelf-item {
display: flex;
gap: 15px;
padding: 15px;
background-color: #fff;
border-radius: 8px;
}
.book-shelf-cover {
width: 44px;
height: 62px;
object-fit: cover;
}
.book-shelf-title {
font-size: 1rem;
margin-bottom: 5px;
}
.blog-list {
max-width: 800px;
margin: 0 auto;
padding: 40px 20px;
}
.blog-entry {
background-color: #fff;
margin-bottom: 30px;
padding: 25px;
border-radius: 8px;
}
.blog-entry-header {
margin-bottom: 15px;
}
.blog-entry-meta {
font-size: 0.875rem;
color: var(--bdm-mid-grey);
}
.blog-entry-meta span {
margin-right: 15px;
}
.books-container,
.series-container,
.reviews-container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
.book-entry {
display: grid;
grid-template-columns: 130px 1fr;
gap: 30px;
background-color: #fff;
margin-bottom: 30px;
padding: 25px;
border-radius: 8px;
}
.book-entry-cover img {
width: 130px;
height: 185px;
object-fit: cover;
}
.book-series-label {
display: inline-block;
font-size: 0.875rem;
color: var(--bdm-slate);
margin-bottom: 10px;
}
.book-title {
margin-bottom: 5px;
}
.book-author {
color: var(--bdm-mid-grey);
margin-bottom: 15px;
}
.book-format-toggle {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.format-btn {
padding: 8px 16px;
background: var(--bdm-parchment);
border: 1px solid var(--bdm-light-grey);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.format-btn.active,
.format-btn:hover {
background: var(--bdm-active-green);
color: #fff;
border-color: var(--bdm-active-green);
}
.book-retailer-links {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 15px;
}
.retailer-link {
padding: 8px 16px;
background-color: var(--bdm-navy);
color: #fff;
border-radius: 4px;
font-size: 0.875rem;
}
.retailer-link:hover {
background-color: var(--bdm-gold);
color: var(--bdm-navy);
}
.book-page-container {
max-width: 1000px;
margin: 0 auto;
padding: 40px 20px;
}
.language-selector {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 30px;
border-bottom: 2px solid var(--bdm-navy);
padding-bottom: 15px;
}
.lang-btn {
padding: 8px 16px;
border: 1px solid var(--bdm-light-grey);
border-radius: 4px;
transition: all 0.2s ease;
}
.lang-btn.active {
background-color: var(--bdm-navy);
color: #fff;
}
.book-page-content {
display: grid;
grid-template-columns: 220px 1fr;
gap: 40px;
}
.book-page-cover img {
width: 220px;
height: 310px;
object-fit: cover;
}
.book-preview {
margin-top: 25px;
padding: 20px;
background-color: var(--bdm-parchment);
border-left: 4px solid var(--bdm-gold);
}
.pull-quote {
font-family: var(--bdm-heading-font);
font-style: italic;
font-size: 1.125rem;
color: var(--bdm-navy);
margin: 20px 0;
padding-left: 20px;
border-left: 3px solid var(--bdm-gold);
}
.review-card {
background-color: var(--bdm-parchment);
padding: 20px;
margin-bottom: 15px;
border-radius: 8px;
}
.review-source {
display: block;
margin-top: 10px;
font-weight: 600;
color: var(--bdm-slate);
text-transform: uppercase;
font-size: 0.875rem;
}
.share-btn {
padding: 12px 24px;
background-color: var(--bdm-navy);
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
.share-btn:hover {
background-color: var(--bdm-gold);
color: var(--bdm-navy);
}
.social-share-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
}
.share-btn svg {
width: 20px;
height: 20px;
}
.social-share-buttons .share-btn {
width: 40px;
height: 40px;
padding: 8px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.social-share-buttons .share-btn svg {
width: 18px;
height: 18px;
fill: #fff;
}
.series-card {
background-color: #fff;
padding: 25px;
margin-bottom: 30px;
border-radius: 8px;
text-align: center;
}
.series-cover {
width: 400px;
height: 280px;
object-fit: cover;
margin-bottom: 15px;
}
.about-container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
display: grid;
grid-template-columns: 66% 34%;
gap: 30px;
}
.about-left-column {
background-color: #fff;
padding: 30px;
border-radius: 8px;
}
.about-sidebar {
position: sticky;
top: 80px;
}
.author-photo-container {
margin-bottom: 30px;
}
.author-photo {
width: 100%;
border-radius: 8px;
}
.author-photo-placeholder {
width: 100%;
max-width: 300px;
}
.signup-section,
.signup-card {
margin-top: 30px;
padding: 20px;
background-color: var(--bdm-parchment);
border-radius: 8px;
}
.signup-btn {
display: inline-block;
padding: 12px 24px;
background-color: var(--bdm-navy);
color: #fff;
border-radius: 4px;
margin-top: 10px;
}
.signup-btn:hover {
background-color: var(--bdm-gold);
color: var(--bdm-navy);
}
.signup-container,
.unsubscribe-container,
.contact-container {
max-width: 600px;
margin: 0 auto;
padding: 60px 20px;
}
.signup-content,
.unsubscribe-content,
.contact-content {
background-color: #fff;
padding: 40px;
border-radius: 8px;
}
.signup-form,
.unsubscribe-form,
.contact-form {
margin-top: 30px;
}
.form-field {
margin-bottom: 20px;
}
.form-field label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
.form-field input[type="email"],
.form-field input[type="text"],
.form-field textarea {
width: 100%;
padding: 12px;
border: 1px solid var(--bdm-light-grey);
border-radius: 4px;
font-size: 1rem;
}
.form-field textarea {
resize: vertical;
}
.checkbox-field {
display: flex;
align-items: flex-start;
gap: 10px;
}
.checkbox-field input {
margin-top: 5px;
}
.honeypot {
position: absolute;
left: -9999px;
}
.submit-btn {
padding: 14px 28px;
background-color: var(--bdm-navy);
color: #fff;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
}
.submit-btn:hover {
background-color: var(--bdm-gold);
color: var(--bdm-navy);
}
.reviews-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 30px;
}
.review-card {
background-color: #fff;
padding: 25px;
border-radius: 8px;
}
.review-card-cover img {
width: 52px;
height: 74px;
object-fit: cover;
margin-bottom: 15px;
}
.review-entry {
margin-bottom: 15px;
}
.review-source {
display: block;
color: var(--bdm-slate);
font-weight: 600;
margin-bottom: 5px;
}
.view-all-reviews {
display: inline-block;
margin-top: 10px;
color: var(--bdm-slate);
text-decoration: underline;
}
.error-container {
max-width: 600px;
margin: 100px auto;
text-align: center;
padding: 40px 20px;
}
.error-code {
font-size: 6rem;
color: var(--bdm-gold);
margin-bottom: 20px;
}
.search-container {
max-width: 800px;
margin: 0 auto;
padding: 40px 20px;
}
.search-entry {
background-color: #fff;
padding: 25px;
margin-bottom: 20px;
border-radius: 8px;
}
@media (max-width: 1199px) {
.home-container,
.about-container {
grid-template-columns: 1fr;
}
.footer-container {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.menu-toggle {
display: block;
}
.nav-menu-container ul {
display: none;
flex-direction: column;
}
.nav-menu-container.toggled ul {
display: flex;
}
.book-entry,
.book-page-content {
grid-template-columns: 1fr;
}
.footer-container {
grid-template-columns: 1fr;
}
.reviews-grid {
grid-template-columns: 1fr;
}
.series-cover {
width: 100%;
max-width: 400px;
}
}
.share-toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background-color: var(--bdm-navy);
color: #fff;
padding: 12px 24px;
border-radius: 4px;
z-index: 1000;
opacity: 0;
transition: all 0.3s ease;
}
.share-toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
.awards-list {
list-style: none;
padding: 0;
}
.award-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
}
.award-dot {
width: 8px;
height: 8px;
background-color: var(--bdm-gold);
border-radius: 50%;
}
.donate-section {
margin-top: 30px;
}
.breadcrumb {
padding: 15px 0;
margin-bottom: 20px;
border-bottom: 1px solid var(--bdm-parchment);
}
.breadcrumb a {
color: var(--bdm-slate);
}
.single-series-container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
.series-header {
margin-bottom: 30px;
}
.series-header-image {
width: 100%;
height: 300px;
object-fit: cover;
border-radius: 8px;
}
.series-books {
display: flex;
flex-direction: column;
gap: 30px;
}
.series-book-entry {
display: grid;
grid-template-columns: 130px 1fr;
gap: 30px;
background-color: #fff;
padding: 25px;
border-radius: 8px;
}
.volume-label {
display: inline-block;
padding: 4px 12px;
background-color: var(--bdm-gold);
color: var(--bdm-navy);
border-radius: 4px;
font-size: 0.875rem;
margin-bottom: 10px;
}
.series-book-cover img {
width: 130px;
height: 185px;
object-fit: cover;
}

View File

@@ -0,0 +1,55 @@
</main>
<footer id="colophon" class="site-footer">
<div class="footer-container">
<div class="footer-zone footer-social">
<h4 class="footer-heading"><?php esc_html_e('Connect', 'bd-murphy'); ?></h4>
<?php bdm_display_social_icons(); ?>
</div>
<div class="footer-zone footer-donate">
<h4 class="footer-heading"><?php esc_html_e('Support', 'bd-murphy'); ?></h4>
<?php bdm_display_donate_buttons(); ?>
<p class="footer-signup-prompt">
<a href="<?php echo esc_url(home_url('/signup/')); ?>"><?php esc_html_e('Sign up for updates', 'bd-murphy'); ?></a>
</p>
</div>
<div class="footer-zone footer-links">
<h4 class="footer-heading"><?php esc_html_e('Links', 'bd-murphy'); ?></h4>
<?php
wp_nav_menu(array(
'theme_location' => 'footer',
'menu_id' => 'footer-menu',
'container' => false,
'depth' => 1,
));
?>
</div>
</div>
<div class="footer-widgets">
<div class="footer-widgets-container">
<?php if (is_active_sidebar('footer-left')) : ?>
<div class="footer-widget-area footer-widget-left">
<?php dynamic_sidebar('footer-left'); ?>
</div>
<?php endif; ?>
<?php if (is_active_sidebar('footer-right')) : ?>
<div class="footer-widget-area footer-widget-right">
<?php dynamic_sidebar('footer-right'); ?>
</div>
<?php endif; ?>
</div>
</div>
<div class="footer-bottom">
<p class="copyright">&copy; <?php echo date('Y'); ?> <?php bloginfo('name'); ?>. <?php esc_html_e('All rights reserved.', 'bd-murphy'); ?></p>
</div>
</footer>
</div>
<?php wp_footer(); ?>
</body>
</html>

View File

@@ -0,0 +1,40 @@
<?php
/**
* Template Name: Home
*/
get_header();
?>
<div class="home-container">
<div class="home-left-column">
<div class="home-content">
<?php
if (have_posts()) :
while (have_posts()) :
the_post();
the_content();
endwhile;
else :
?>
<div class="no-content">
<h2><?php esc_html_e('Welcome', 'bd-murphy'); ?></h2>
<p><?php esc_html_e('Latest news and updates from the author.', 'bd-murphy'); ?></p>
</div>
<?php
endif;
?>
</div>
</div>
<aside class="home-right-column">
<?php if (is_active_sidebar('home-sidebar')) : ?>
<div class="home-sidebar">
<?php dynamic_sidebar('home-sidebar'); ?>
</div>
<?php endif; ?>
</aside>
</div>
<?php
get_footer();

View File

@@ -0,0 +1,158 @@
<?php
/**
* BD Murphy Bookshelf Theme Functions
*
* @package BD_Murphy
*/
if (!defined('ABSPATH')) {
exit;
}
define('BDM_VERSION', '1.5.0');
define('BDM_DIR', get_template_directory());
define('BDM_URI', get_template_directory_uri());
require_once BDM_DIR . '/inc/customizer.php';
require_once BDM_DIR . '/inc/theme-options.php';
require_once BDM_DIR . '/inc/template-tags.php';
require_once BDM_DIR . '/inc/widgets.php';
require_once BDM_DIR . '/inc/nav-menu.php';
function bdm_setup() {
load_theme_textdomain('bd-murphy', BDM_DIR . '/languages');
add_theme_support('title-tag');
add_theme_support('post-thumbnails');
add_theme_support('html5', array('search-form', 'comment-form', 'comment-list', 'gallery', 'caption', 'script', 'style'));
add_theme_support('automatic-feed-links');
add_theme_support('wp-block-styles');
add_editor_style('assets/editor-style.css');
add_theme_support('custom-logo', array(
'width' => 200,
'height' => 200,
'flex-width' => true,
'flex-height' => true,
));
add_theme_support('custom-header', array(
'default-image' => BDM_URI . '/assets/banner_1200x200_v3.png',
'width' => 1200,
'height' => 200,
'flex-height' => false,
));
add_theme_support('custom-background', array(
'default-color' => 'F5F0E8',
));
add_image_size('shelf-thumb', 44, 62, true);
add_image_size('blog-thumb', 100, 68, true);
add_image_size('rev-thumb', 52, 74, true);
add_image_size('book-cover', 130, 185, true);
add_image_size('book-page-cover', 220, 310, true);
add_image_size('series-card', 400, 280, true);
add_image_size('series-header', 1200, 300, true);
register_nav_menus(array(
'primary' => __('Primary Menu', 'bd-murphy'),
'footer' => __('Footer Menu', 'bd-murphy'),
));
register_sidebar(array(
'name' => __('Home Sidebar', 'bd-murphy'),
'id' => 'home-sidebar',
'description' => __('Widget area for the book shelf on the home page', 'bd-murphy'),
'before_widget' => '<div id="%1$s" class="widget %2$s">',
'after_widget' => '</div>',
'before_title' => '<h3 class="widget-title">',
'after_title' => '</h3>',
));
register_sidebar(array(
'name' => __('Footer Left', 'bd-murphy'),
'id' => 'footer-left',
'before_widget' => '<div id="%1$s" class="widget %2$s">',
'after_widget' => '</div>',
));
register_sidebar(array(
'name' => __('Footer Right', 'bd-murphy'),
'id' => 'footer-right',
'before_widget' => '<div id="%1$s" class="widget %2$s">',
'after_widget' => '</div>',
));
}
add_action('after_setup_theme', 'bdm_setup');
function bdm_enqueue_assets() {
wp_enqueue_style('bd-murphy-style', get_stylesheet_uri(), array(), BDM_VERSION);
wp_enqueue_style('bd-murphy-google-fonts', 'https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400&family=Lato:wght@300;400;700&display=swap', array(), null);
wp_enqueue_script('bd-murphy-navigation', BDM_URI . '/assets/js/navigation.js', array(), BDM_VERSION, true);
wp_enqueue_script('bd-murphy-social-share', BDM_URI . '/assets/js/social-share.js', array(), BDM_VERSION, true);
wp_localize_script('bd-murphy-navigation', 'bdMurphyScreenReaderText', array(
'expand' => __('Expand child menu', 'bd-murphy'),
'collapse' => __('Collapse child menu', 'bd-murphy'),
));
}
add_action('wp_enqueue_scripts', 'bdm_enqueue_assets');
function bdm_enqueue_admin_assets($hook) {
wp_enqueue_style('bd-murphy-admin-style', BDM_URI . '/assets/admin.css', array(), BDM_VERSION);
}
add_action('admin_enqueue_scripts', 'bdm_enqueue_admin_assets');
function bdm_output_customizer_css() {
$primary_color = get_theme_mod('primary_color', '#1B3A6B');
$accent_color = get_theme_mod('accent_color', '#C8A96E');
$cream_color = get_theme_mod('background_color', 'F5F0E8');
$heading_font = get_theme_mod('heading_font', 'Playfair Display');
$body_font = get_theme_mod('body_font', 'Lato');
$slate_blue = bdm_adjust_color($primary_color, 30);
$sky_blue = bdm_adjust_color($primary_color, 80);
echo '<style id="bdm-customizer-styles" type="text/css">' . "\n";
echo ':root {' . "\n";
echo '--bdm-navy: ' . esc_attr($primary_color) . ';' . "\n";
echo '--bdm-slate: ' . esc_attr($slate_blue) . ';' . "\n";
echo '--bdm-sky: ' . esc_attr($sky_blue) . ';' . "\n";
echo '--bdm-gold: ' . esc_attr($accent_color) . ';' . "\n";
echo '--bdm-cream: ' . esc_attr($cream_color) . ';' . "\n";
echo '--bdm-parchment: #EDE4D0;' . "\n";
echo '--bdm-body-text: #2C2C2C;' . "\n";
echo '--bdm-mid-grey: #555555;' . "\n";
echo '--bdm-light-grey: #888888;' . "\n";
echo '--bdm-active-green: #2D6A4F;' . "\n";
echo '--bdm-heading-font: ' . esc_attr($heading_font) . ', Georgia, serif;' . "\n";
echo '--bdm-body-font: ' . esc_attr($body_font) . ', Calibri, sans-serif;' . "\n";
echo '}' . "\n";
echo '</style>' . "\n";
}
add_action('wp_head', 'bdm_output_customizer_css', 99);
function bdm_adjust_color($hex, $percent) {
$hex = ltrim($hex, '#');
$r = hexdec(substr($hex, 0, 2));
$g = hexdec(substr($hex, 2, 2));
$b = hexdec(substr($hex, 4, 2));
$r = min(255, max(0, $r + $percent));
$g = min(255, max(0, $g + $percent));
$b = min(255, max(0, $b + $percent));
return sprintf('#%02x%02x%02x', $r, $g, $b);
}
function bdm_publish_hook($post_id) {
do_action('bdm_post_published', $post_id);
}
add_action('publish_post', 'bdm_publish_hook');
add_action('wp_head', function() {
echo '<link rel="alternate" type="application/rss+xml" title="' . get_bloginfo('name') . ' RSS Feed" href="' . esc_url(get_bloginfo('rss2_url')) . '">' . "\n";
});

View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="profile" href="https://gmpg.org/xfn/11">
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<div id="page" class="site">
<header id="masthead" class="site-header">
<?php if (get_header_image()) : ?>
<div class="header-banner">
<img src="<?php header_image(); ?>" alt="<?php bloginfo('name'); ?>" class="header-image">
</div>
<?php endif; ?>
<nav id="site-nav" class="site-nav" role="navigation" aria-label="<?php esc_attr_e('Primary Menu', 'bd-murphy'); ?>">
<div class="nav-container">
<button class="menu-toggle" aria-controls="primary-menu" aria-expanded="false">
<span class="screen-reader-text"><?php esc_html_e('Menu', 'bd-murphy'); ?></span>
<span class="hamburger"></span>
</button>
<?php
wp_nav_menu(array(
'theme_location' => 'primary',
'menu_id' => 'primary-menu',
'container_class' => 'nav-menu-container',
));
?>
</div>
</nav>
</header>
<main id="main" class="site-main">

View File

@@ -0,0 +1,147 @@
<?php
/**
* Customizer Settings
*
* @package BD_Murphy
*/
if (!defined('ABSPATH')) {
exit;
}
function bdm_customize_register($wp_customize) {
$wp_customize->add_panel('bdm_colors', array(
'title' => __('Colors', 'bd-murphy'),
'priority' => 30,
));
$wp_customize->add_section('bdm_primary_colors', array(
'title' => __('Primary Colors', 'bd-murphy'),
'panel' => 'bdm_colors',
));
$wp_customize->add_setting('primary_color', array(
'default' => '#1B3A6B',
'sanitize_callback' => 'sanitize_hex_color',
));
$wp_customize->add_control(new WP_Customize_Color_Control($wp_customize, 'primary_color', array(
'label' => __('Primary Color (Navy)', 'bd-murphy'),
'section' => 'bdm_primary_colors',
)));
$wp_customize->add_setting('accent_color', array(
'default' => '#C8A96E',
'sanitize_callback' => 'sanitize_hex_color',
));
$wp_customize->add_control(new WP_Customize_Color_Control($wp_customize, 'accent_color', array(
'label' => __('Accent Color (Gold)', 'bd-murphy'),
'section' => 'bdm_primary_colors',
)));
$wp_customize->add_panel('bdm_typography', array(
'title' => __('Typography', 'bd-murphy'),
'priority' => 35,
));
$wp_customize->add_section('bdm_heading_fonts', array(
'title' => __('Headings', 'bd-murphy'),
'panel' => 'bdm_typography',
));
$heading_fonts = array(
'Playfair Display' => 'Playfair Display',
'Merriweather' => 'Merriweather',
'Libre Baskerville' => 'Libre Baskerville',
'Lora' => 'Lora',
'Georgia' => 'Georgia',
);
$wp_customize->add_setting('heading_font', array(
'default' => 'Playfair Display',
'sanitize_callback' => 'sanitize_text_field',
));
$wp_customize->add_control('heading_font', array(
'label' => __('Heading Font', 'bd-murphy'),
'section' => 'bdm_heading_fonts',
'type' => 'select',
'choices' => $heading_fonts,
));
$wp_customize->add_section('bdm_body_fonts', array(
'title' => __('Body', 'bd-murphy'),
'panel' => 'bdm_typography',
));
$body_fonts = array(
'Lato' => 'Lato',
'Open Sans' => 'Open Sans',
'Source Sans Pro' => 'Source Sans Pro',
'Roboto' => 'Roboto',
'Calibri' => 'Calibri',
);
$wp_customize->add_setting('body_font', array(
'default' => 'Lato',
'sanitize_callback' => 'sanitize_text_field',
));
$wp_customize->add_control('body_font', array(
'label' => __('Body Font', 'bd-murphy'),
'section' => 'bdm_body_fonts',
'type' => 'select',
'choices' => $body_fonts,
));
$wp_customize->add_section('bdm_social_sharing', array(
'title' => __('Social Sharing', 'bd-murphy'),
'priority' => 60,
));
$wp_customize->add_setting('share_facebook', array(
'default' => true,
'sanitize_callback' => 'wp_validate_boolean',
));
$wp_customize->add_control('share_facebook', array(
'label' => __('Show Facebook', 'bd-murphy'),
'section' => 'bdm_social_sharing',
'type' => 'checkbox',
));
$wp_customize->add_setting('share_twitter', array(
'default' => true,
'sanitize_callback' => 'wp_validate_boolean',
));
$wp_customize->add_control('share_twitter', array(
'label' => __('Show X/Twitter', 'bd-murphy'),
'section' => 'bdm_social_sharing',
'type' => 'checkbox',
));
$wp_customize->add_setting('share_linkedin', array(
'default' => true,
'sanitize_callback' => 'wp_validate_boolean',
));
$wp_customize->add_control('share_linkedin', array(
'label' => __('Show LinkedIn', 'bd-murphy'),
'section' => 'bdm_social_sharing',
'type' => 'checkbox',
));
$wp_customize->add_setting('share_whatsapp', array(
'default' => true,
'sanitize_callback' => 'wp_validate_boolean',
));
$wp_customize->add_control('share_whatsapp', array(
'label' => __('Show WhatsApp', 'bd-murphy'),
'section' => 'bdm_social_sharing',
'type' => 'checkbox',
));
}
add_action('customize_register', 'bdm_customize_register');

View File

@@ -0,0 +1,51 @@
<?php
/**
* Navigation Menu Hooks
*
* @package BD_Murphy
*/
if (!defined('ABSPATH')) {
exit;
}
add_filter('wp_nav_menu_items', 'bdm_filter_nav_menu_items', 10, 2);
function bdm_filter_nav_menu_items($items, $args) {
if ($args->theme_location !== 'primary') {
return $items;
}
if (bdm_series_exist()) {
$series_item = '<li class="menu-item menu-item-type-taxonomy"><a href="' . home_url('/series/') . '">' . __('Series', 'bd-murphy') . '</a></li>';
$books_pos = strpos($items, 'Books');
if ($books_pos !== false) {
$before_books = substr($items, 0, $books_pos);
$from_books = substr($items, $books_pos);
$books_end = strpos($from_books, '</li>');
$books_item = substr($from_books, 0, $books_end + 5);
$rest = substr($from_books, $books_end + 5);
return $before_books . $books_item . $series_item . $rest;
}
}
return $items;
}
function bdm_series_exist() {
if (class_exists('BD_BookShelfHelper')) {
return BD_BookShelfHelper::series_count() > 0;
}
return false;
}
add_filter('nav_menu_css_class', 'bdm_nav_menu_css_class', 10, 4);
function bdm_nav_menu_css_class($classes, $item, $args, $depth) {
if ($item->url && strpos($item->url, '/series/') !== false) {
$classes[] = 'series-menu-item';
}
return $classes;
}

View File

@@ -0,0 +1,137 @@
<?php
/**
* Template Tags
*
* @package BD_Murphy
*/
if (!defined('ABSPATH')) {
exit;
}
function bdm_reading_time($post_id = null) {
if (!$post_id) {
$post_id = get_the_ID();
}
$content = get_post_field('post_content', $post_id);
$word_count = str_word_count(strip_tags($content));
$reading_time = ceil($word_count / 200);
return $reading_time;
}
function bdm_display_reading_time($post_id = null) {
$minutes = bdm_reading_time($post_id);
$label = $minutes === 1 ? __('min read', 'bd-murphy') : __('min read', 'bd-murphy');
return sprintf('%d %s', $minutes, $label);
}
function bdm_posted_on() {
$time_string = '<time class="entry-date" datetime="%1$s">%2$s</time>';
echo sprintf($time_string,
esc_attr(get_the_date(DATE_W3C)),
esc_html(get_the_date())
);
}
function bdm_posted_by() {
echo '<span class="author vcard"><a class="url fn n" href="' . esc_url(get_author_posts_url(get_the_author_meta('ID'))) . '">' . esc_html(get_the_author()) . '</a></span>';
}
function bdm_entry_footer() {
if ('post' === get_post_type()) {
$categories_list = get_the_category_list(__(', ', 'bd-murphy'));
if ($categories_list && !is_wp_error($categories_list)) {
echo '<span class="cat-links">' . $categories_list . '</span>';
}
}
}
function bdm_get_social_urls() {
return array(
'twitter' => bdm_get_theme_option('twitter_url'),
'facebook' => bdm_get_theme_option('facebook_url'),
'linkedin' => bdm_get_theme_option('linkedin_url'),
'instagram' => bdm_get_theme_option('instagram_url'),
);
}
function bdm_display_social_icons($echo = true) {
$urls = bdm_get_social_urls();
$output = '';
foreach ($urls as $platform => $url) {
if (!empty($url)) {
$icon = '';
switch ($platform) {
case 'twitter':
$icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>';
break;
case 'facebook':
$icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>';
break;
case 'linkedin':
$icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.678 2.678 0 01-2.643-2.68 2.68 2.68 0 112.68 2.68zm1.5 13.019H3.823V9h2.014v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>';
break;
case 'instagram':
$icon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg>';
break;
}
$output .= '<a href="' . esc_url($url) . '" class="social-link social-' . esc_attr($platform) . '" target="_blank" rel="noopener noreferrer" aria-label="' . esc_attr(ucfirst($platform)) . '">' . $icon . '</a>';
}
}
if ($echo) {
echo $output;
}
return $output;
}
function bdm_get_donate_urls() {
return array(
'buymeacoffee' => bdm_get_theme_option('buymeacoffee_url'),
'paypal' => bdm_get_theme_option('paypal_url'),
);
}
function bdm_display_donate_buttons($echo = true) {
$urls = bdm_get_donate_urls();
$output = '';
if (!empty($urls['buymeacoffee'])) {
$output .= '<a href="' . esc_url($urls['buymeacoffee']) . '" class="donate-btn donate-buymeacoffee" target="_blank" rel="noopener noreferrer">' . __('Buy Me a Coffee', 'bd-murphy') . '</a>';
}
if (!empty($urls['paypal'])) {
$output .= '<a href="' . esc_url($urls['paypal']) . '" class="donate-btn donate-paypal" target="_blank" rel="noopener noreferrer">' . __('Donate via PayPal', 'bd-murphy') . '</a>';
}
if ($echo) {
echo $output;
}
return $output;
}
function bdm_display_author_photo() {
$photo_id = bdm_get_theme_option('author_photo_id');
if ($photo_id) {
$photo_url = wp_get_attachment_image_url($photo_id, 'medium');
if ($photo_url) {
echo '<img src="' . esc_url($photo_url) . '" alt="' . esc_attr__('Author photo', 'bd-murphy') . '" class="author-photo">';
return;
}
}
echo '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" class="author-photo-placeholder">
<circle cx="50" cy="35" r="20" fill="#C8A96E"/>
<path d="M50 65c-22 0-40 18-40 40h80c0-22-18-40-40-40z" fill="#C8A96E"/>
</svg>';
}
function bdm_get_short_url($post_id = null) {
if (!$post_id) {
$post_id = get_the_ID();
}
$slug = get_post_meta($post_id, 'short_link_slug', true);
if ($slug) {
return home_url('/' . $slug);
}
return get_permalink($post_id);
}

View File

@@ -0,0 +1,140 @@
<?php
/**
* Theme Options Admin Page
*
* @package BD_Murphy
*/
if (!defined('ABSPATH')) {
exit;
}
function bdm_theme_options_menu() {
add_theme_page(
__('Theme Options', 'bd-murphy'),
__('Theme Options', 'bd-murphy'),
'manage_options',
'bdm-theme-options',
'bdm_theme_options_page'
);
}
add_action('admin_menu', 'bdm_theme_options_menu');
function bdm_register_theme_options() {
register_setting('bdm_theme_options_group', 'bdm_theme_options', 'bdm_sanitize_theme_options');
add_settings_section('bdm_social_section', __('Social Media Links', 'bd-murphy'), 'bdm_social_section_callback', 'bdm-theme-options');
add_settings_section('bdm_donate_section', __('Donation Links', 'bd-murphy'), 'bdm_donate_section_callback', 'bdm-theme-options');
add_settings_section('bdm_author_section', __('Author Settings', 'bd-murphy'), 'bdm_author_section_callback', 'bdm-theme-options');
add_settings_section('bdm_business_section', __('Business Information (CAN-SPAM)', 'bd-murphy'), 'bdm_business_section_callback', 'bdm-theme-options');
add_settings_section('bdm_navigation_section', __('Navigation Settings', 'bd-murphy'), 'bdm_navigation_section_callback', 'bdm-theme-options');
add_settings_field('twitter_url', __('X/Twitter URL', 'bd-murphy'), 'bdm_text_field_callback', 'bdm-theme-options', 'bdm_social_section', array('key' => 'twitter_url', 'label' => __('X/Twitter profile URL', 'bd-murphy')));
add_settings_field('facebook_url', __('Facebook URL', 'bd-murphy'), 'bdm_text_field_callback', 'bdm-theme-options', 'bdm_social_section', array('key' => 'facebook_url', 'label' => __('Facebook page URL', 'bd-murphy')));
add_settings_field('linkedin_url', __('LinkedIn URL', 'bd-murphy'), 'bdm_text_field_callback', 'bdm-theme-options', 'bdm_social_section', array('key' => 'linkedin_url', 'label' => __('LinkedIn profile URL', 'bd-murphy')));
add_settings_field('instagram_url', __('Instagram URL', 'bd-murphy'), 'bdm_text_field_callback', 'bdm-theme-options', 'bdm_social_section', array('key' => 'instagram_url', 'label' => __('Instagram profile URL', 'bd-murphy')));
add_settings_field('buymeacoffee_url', __('Buy Me a Coffee URL', 'bd-murphy'), 'bdm_text_field_callback', 'bdm-theme-options', 'bdm_donate_section', array('key' => 'buymeacoffee_url', 'label' => __('Buy Me a Coffee page URL', 'bd-murphy')));
add_settings_field('paypal_url', __('PayPal URL', 'bd-murphy'), 'bdm_text_field_callback', 'bdm-theme-options', 'bdm_donate_section', array('key' => 'paypal_url', 'label' => __('PayPal.me or donation URL', 'bd-murphy')));
add_settings_field('author_photo_id', __('Author Photo', 'bd-murphy'), 'bdm_media_field_callback', 'bdm-theme-options', 'bdm_author_section', array('key' => 'author_photo_id', 'label' => __('Author photo (WordPress media attachment ID)', 'bd-murphy')));
add_settings_field('business_name', __('Business Name', 'bd-murphy'), 'bdm_text_field_callback', 'bdm-theme-options', 'bdm_business_section', array('key' => 'business_name', 'label' => __('Legal business name', 'bd-murphy')));
add_settings_field('business_address_1', __('Address Line 1', 'bd-murphy'), 'bdm_text_field_callback', 'bdm-theme-options', 'bdm_business_section', array('key' => 'business_address_1', 'label' => __('Street address', 'bd-murphy')));
add_settings_field('business_address_2', __('Address Line 2', 'bd-murphy'), 'bdm_text_field_callback', 'bdm-theme-options', 'bdm_business_section', array('key' => 'business_address_2', 'label' => __('Suite, unit, PO box', 'bd-murphy')));
add_settings_field('business_city', __('City / State / Postal', 'bd-murphy'), 'bdm_text_field_callback', 'bdm-theme-options', 'bdm_business_section', array('key' => 'business_city', 'label' => __('City / State / Postal code', 'bd-murphy')));
add_settings_field('business_country', __('Country', 'bd-murphy'), 'bdm_text_field_callback', 'bdm-theme-options', 'bdm_business_section', array('key' => 'business_country', 'label' => __('Country', 'bd-murphy')));
add_settings_field('business_phone', __('Phone Number', 'bd-murphy'), 'bdm_text_field_callback', 'bdm-theme-options', 'bdm_business_section', array('key' => 'business_phone', 'label' => __('Phone number (optional)', 'bd-murphy')));
add_settings_field('sticky_nav', __('Sticky Navigation', 'bd-murphy'), 'bdm_checkbox_field_callback', 'bdm-theme-options', 'bdm_navigation_section', array('key' => 'sticky_nav', 'label' => __('Enable sticky navigation', 'bd-murphy'), 'default' => true));
add_settings_field('posts_per_page', __('Posts Per Page', 'bd-murphy'), 'bdm_number_field_callback', 'bdm-theme-options', 'bdm_navigation_section', array('key' => 'posts_per_page', 'label' => __('Blog posts per page', 'bd-murphy'), 'default' => 10));
add_settings_field('show_read_time', __('Read Time', 'bd-murphy'), 'bdm_checkbox_field_callback', 'bdm-theme-options', 'bdm_navigation_section', array('key' => 'show_read_time', 'label' => __('Show read time on posts', 'bd-murphy'), 'default' => true));
}
add_action('admin_init', 'bdm_register_theme_options');
function bdm_get_theme_option($key, $default = '') {
$options = get_option('bdm_theme_options', array());
return isset($options[$key]) ? $options[$key] : $default;
}
function bdm_sanitize_theme_options($input) {
$sanitized = array();
foreach ($input as $key => $value) {
if (in_array($key, array('buymeacoffee_url', 'paypal_url', 'twitter_url', 'facebook_url', 'linkedin_url', 'instagram_url'))) {
$sanitized[$key] = esc_url_raw($value);
} elseif ($key === 'business_phone') {
$sanitized[$key] = sanitize_text_field($value);
} else {
$sanitized[$key] = sanitize_text_field($value);
}
}
return $sanitized;
}
function bdm_theme_options_page() {
if (!current_user_can('manage_options')) {
return;
}
?>
<div class="wrap">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<form method="post" action="options.php">
<?php
settings_fields('bdm_theme_options_group');
do_settings_sections('bdm-theme-options');
submit_button();
?>
</form>
</div>
<?php
}
function bdm_social_section_callback() {
echo '<p>' . esc_html__('Enter your social media profile URLs.', 'bd-murphy') . '</p>';
}
function bdm_donate_section_callback() {
echo '<p>' . esc_html__('Enter your donation page URLs.', 'bd-murphy') . '</p>';
}
function bdm_author_section_callback() {
echo '<p>' . esc_html__('Configure author settings.', 'bd-murphy') . '</p>';
}
function bdm_business_section_callback() {
echo '<p>' . esc_html__('Business information required for CAN-SPAM compliance.', 'bd-murphy') . '</p>';
}
function bdm_navigation_section_callback() {
echo '<p>' . esc_html__('Configure navigation and blog settings.', 'bd-murphy') . '</p>';
}
function bdm_text_field_callback($args) {
$key = $args['key'];
$value = bdm_get_theme_option($key);
echo '<input type="text" id="bdm_' . esc_attr($key) . '" name="bdm_theme_options[' . esc_attr($key) . ']" value="' . esc_attr($value) . '" class="regular-text">';
if (!empty($args['label'])) {
echo '<p class="description">' . esc_html($args['label']) . '</p>';
}
}
function bdm_checkbox_field_callback($args) {
$key = $args['key'];
$value = bdm_get_theme_option($key, $args['default'] ?? false);
echo '<input type="checkbox" id="bdm_' . esc_attr($key) . '" name="bdm_theme_options[' . esc_attr($key) . ']" value="1"' . checked($value, true, false) . '>';
echo '<label for="bdm_' . esc_attr($key) . '">' . esc_html($args['label']) . '</label>';
}
function bdm_number_field_callback($args) {
$key = $args['key'];
$value = bdm_get_theme_option($key, $args['default'] ?? 10);
echo '<input type="number" id="bdm_' . esc_attr($key) . '" name="bdm_theme_options[' . esc_attr($key) . ']" value="' . esc_attr($value) . '" class="small-text">';
echo '<p class="description">' . esc_html($args['label']) . '</p>';
}
function bdm_media_field_callback($args) {
$key = $args['key'];
$value = bdm_get_theme_option($key);
echo '<input type="text" id="bdm_' . esc_attr($key) . '" name="bdm_theme_options[' . esc_attr($key) . ']" value="' . esc_attr($value) . '" class="small-text">';
echo '<p class="description">' . esc_html($args['label']) . '</p>';
}

View File

@@ -0,0 +1,99 @@
<?php
/**
* Widgets
*
* @package BD_Murphy
*/
if (!defined('ABSPATH')) {
exit;
}
class BDM_Book_Shelf_Widget extends WP_Widget {
public function __construct() {
parent::__construct(
'bdm_book_shelf',
__('Book Shelf', 'bd-murphy'),
array('description' => __('Displays book covers from BD BookHelper', 'bd-murphy'))
);
}
public function widget($args, $instance) {
echo $args['before_widget'];
if (!empty($instance['title'])) {
echo $args['before_title'] . apply_filters('widget_title', $instance['title']) . $args['after_title'];
}
if (class_exists('BD_BookShelfHelper')) {
$books = BD_BookShelfHelper::get_featured_books(5);
if (!empty($books)) {
echo '<div class="book-shelf-widget">';
foreach ($books as $book) {
$cover_url = BD_BookShelfHelper::get_cover_url($book->id, 'shelf-thumb');
$title = esc_html($book->title);
$url = BD_BookShelfHelper::get_book_url($book->id);
$ratings = BD_BookShelfHelper::get_ratings($book->id);
$retailers = BD_BookShelfHelper::get_retailer_links($book->id);
echo '<div class="book-shelf-item">';
echo '<a href="' . esc_url($url) . '">';
echo '<img src="' . esc_url($cover_url) . '" alt="' . $title . '" class="book-shelf-cover">';
echo '</a>';
echo '<div class="book-shelf-info">';
echo '<h4 class="book-shelf-title"><a href="' . esc_url($url) . '">' . $title . '</a></h4>';
if ($ratings) {
echo '<div class="book-shelf-ratings">';
if (!empty($ratings['amazon'])) {
echo '<span class="rating amazon">Amazon: ' . esc_html($ratings['amazon']) . '</span>';
}
if (!empty($ratings['goodreads'])) {
echo '<span class="rating goodreads">Goodreads: ' . esc_html($ratings['goodreads']) . '</span>';
}
echo '</div>';
}
if ($retailers) {
echo '<div class="book-shelf-retailers">';
foreach ($retailers as $retailer) {
echo '<a href="' . esc_url($retailer['url']) . '" class="retailer-icon retailer-' . esc_attr($retailer['name']) . '" target="_blank" rel="noopener">' . esc_html($retailer['name']) . '</a>';
}
echo '</div>';
}
echo '</div>';
echo '</div>';
}
echo '</div>';
} else {
echo '<p class="no-books">' . __('No books available yet.', 'bd-murphy') . '</p>';
}
} else {
echo '<p class="book-helper-notice">' . __('BD BookHelper plugin is required for this widget.', 'bd-murphy') . '</p>';
}
echo $args['after_widget'];
}
public function form($instance) {
$title = !empty($instance['title']) ? $instance['title'] : __('Book Shelf', 'bd-murphy');
?>
<p>
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php esc_html_e('Title:', 'bd-murphy'); ?></label>
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" type="text" value="<?php echo esc_attr($title); ?>">
</p>
<?php
}
public function update($new_instance, $old_instance) {
$instance = array();
$instance['title'] = (!empty($new_instance['title'])) ? sanitize_text_field($new_instance['title']) : '';
return $instance;
}
}
function bdm_register_widgets() {
register_widget('BDM_Book_Shelf_Widget');
}
add_action('widgets_init', 'bdm_register_widgets');

View File

@@ -0,0 +1,59 @@
<?php
get_header();
?>
<div class="content-area">
<main id="primary" class="site-main">
<?php
if (have_posts()) :
if (is_home() && !is_front_page()) :
?>
<header>
<h1 class="page-title"><?php single_post_title(); ?></h1>
</header>
<?php
endif;
while (have_posts()) :
the_post();
?>
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
<?php if (has_post_thumbnail()) : ?>
<div class="post-thumbnail">
<a href="<?php the_permalink(); ?>">
<?php the_post_thumbnail(); ?>
</a>
</div>
<?php endif; ?>
<header class="entry-header">
<h2 class="entry-title">
<a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
</h2>
</header>
<div class="entry-summary">
<?php the_excerpt(); ?>
</div>
</article>
<?php
endwhile;
the_posts_navigation();
else :
?>
<p class="no-posts"><?php esc_html_e('No posts found.', 'bd-murphy'); ?></p>
<?php
endif;
?>
</main>
</div>
<?php
get_footer();

View File

@@ -0,0 +1,61 @@
<?php
/**
* Template Name: About
*/
get_header();
?>
<div class="about-container">
<div class="about-left-column">
<?php
if (have_posts()) :
while (have_posts()) :
the_post();
the_content();
?>
<div class="awards-section">
<h3><?php esc_html_e('Awards & Recognition', 'bd-murphy'); ?></h3>
<ul class="awards-list">
<?php
$awards = BD_BookShelfHelper::get_awards();
foreach ($awards as $award) {
echo '<li class="award-item"><span class="award-dot"></span>' . esc_html($award) . '</li>';
}
?>
</ul>
</div>
<div class="donate-section">
<?php bdm_display_donate_buttons(); ?>
</div>
<div class="signup-section">
<h4><?php esc_html_e('Stay Updated', 'bd-murphy'); ?></h4>
<p><?php esc_html_e('Sign up to receive updates about new releases, news, and exclusive content.', 'bd-murphy'); ?></p>
<a href="<?php echo esc_url(home_url('/signup/')); ?>" class="signup-btn"><?php esc_html_e('Sign up for updates', 'bd-murphy'); ?></a>
</div>
<?php
endwhile;
endif;
?>
</div>
<aside class="about-right-column">
<div class="about-sidebar">
<div class="author-photo-container">
<?php bdm_display_author_photo(); ?>
</div>
<div class="signup-card">
<h4><?php esc_html_e('Newsletter', 'bd-murphy'); ?></h4>
<p><?php esc_html_e('Get notified about new releases and updates.', 'bd-murphy'); ?></p>
<a href="<?php echo esc_url(home_url('/signup/')); ?>" class="signup-btn"><?php esc_html_e('Sign up for updates', 'bd-murphy'); ?></a>
</div>
</div>
</aside>
</div>
<?php
get_footer();

View File

@@ -0,0 +1,86 @@
<?php
/**
* Template Name: Contact
*/
get_header();
?>
<div class="contact-container">
<div class="contact-content">
<h1><?php esc_html_e('Contact', 'bd-murphy'); ?></h1>
<form id="contact-form" class="contact-form" method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<?php wp_nonce_field('bdm_contact', 'bdm_contact_nonce'); ?>
<input type="hidden" name="action" value="bdm_contact_submit">
<div class="form-field">
<label for="name"><?php esc_html_e('Name', 'bd-murphy'); ?> *</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-field">
<label for="email"><?php esc_html_e('Email', 'bd-murphy'); ?> *</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-field">
<label for="subject"><?php esc_html_e('Subject', 'bd-murphy'); ?> *</label>
<input type="text" id="subject" name="subject" required>
</div>
<div class="form-field honeypot">
<label for="website"><?php esc_html_e('Website', 'bd-murphy'); ?></label>
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off">
</div>
<div class="form-field">
<label for="message"><?php esc_html_e('Message', 'bd-murphy'); ?> *</label>
<textarea id="message" name="message" rows="8" required></textarea>
</div>
<div class="form-submit">
<button type="submit" class="submit-btn"><?php esc_html_e('Send Message', 'bd-murphy'); ?></button>
</div>
</form>
<div id="contact-form-response" class="form-response" style="display:none;"></div>
</div>
</div>
<script>
jQuery(document).ready(function($) {
$('#contact-form').on('submit', function(e) {
e.preventDefault();
if ($('#website').val() !== '') {
return;
}
var formData = $(this).serialize();
var responseDiv = $('#contact-form-response');
$.ajax({
type: 'POST',
url: '<?php echo admin_url('admin-ajax.php'); ?>',
data: formData,
success: function(response) {
if (response.success) {
responseDiv.html('<p class="success"><?php esc_html_e('Thank you for your message. We will get back to you soon.', 'bd-murphy'); ?></p>');
$('#contact-form')[0].reset();
} else {
responseDiv.html('<p class="error"><?php esc_html_e('There was an error sending your message. Please try again.', 'bd-murphy'); ?></p>');
}
responseDiv.show();
},
error: function() {
responseDiv.html('<p class="error"><?php esc_html_e('There was an error sending your message. Please try again.', 'bd-murphy'); ?></p>');
responseDiv.show();
}
});
});
});
</script>
<?php
get_footer();

View File

@@ -0,0 +1,21 @@
<?php
/**
* Template Name: Full Width
*/
get_header();
?>
<div class="page-container full-width">
<div class="page-content">
<?php
while (have_posts()) :
the_post();
the_content();
endwhile;
?>
</div>
</div>
<?php
get_footer();

View File

@@ -0,0 +1,21 @@
<?php
/**
* Template Name: No Sidebar
*/
get_header();
?>
<div class="page-container no-sidebar">
<div class="page-content">
<?php
while (have_posts()) :
the_post();
the_content();
endwhile;
?>
</div>
</div>
<?php
get_footer();

View File

@@ -0,0 +1,62 @@
<?php
/**
* Template Name: Reviews
*/
get_header();
?>
<div class="reviews-container">
<header class="page-header">
<h1 class="page-title"><?php esc_html_e('Reviews', 'bd-murphy'); ?></h1>
</header>
<div class="reviews-grid">
<?php
if (have_posts()) :
while (have_posts()) :
the_post();
$book_id = get_the_ID();
$cover_url = BD_BookShelfHelper::get_cover_url($book_id, 'rev-thumb');
$title = get_the_title();
$reviews = BD_BookShelfHelper::get_reviews($book_id);
$review_count = count($reviews);
?>
<article class="review-card">
<div class="review-card-cover">
<a href="<?php the_permalink(); ?>">
<img src="<?php echo esc_url($cover_url); ?>" alt="<?php echo esc_attr($title); ?>">
</a>
</div>
<div class="review-card-info">
<h2 class="review-card-title">
<a href="<?php the_permalink(); ?>"><?php echo esc_html($title); ?></a>
</h2>
<p class="review-count"><?php printf(esc_html(_n('%d review', '%d reviews', $review_count, 'bd-murphy')), $review_count); ?></p>
<?php if ($reviews) : ?>
<?php $display_reviews = array_slice($reviews, 0, 3); ?>
<?php foreach ($display_reviews as $review) : ?>
<div class="review-entry">
<span class="review-source"><?php echo esc_html(strtoupper($review->source_name)); ?></span>
<p class="review-synopsis"><?php echo esc_html($review->synopsis); ?></p>
</div>
<?php endforeach; ?>
<?php endif; ?>
<a href="<?php the_permalink(); ?>" class="view-all-reviews">
<?php esc_html_e('View all reviews', 'bd-murphy'); ?>
</a>
</div>
</article>
<?php
endwhile;
else :
?>
<p class="no-reviews"><?php esc_html_e('No reviews available yet.', 'bd-murphy'); ?></p>
<?php endif; ?>
</div>
</div>
<?php
get_footer();

View File

@@ -0,0 +1,47 @@
<?php
/**
* Template Name: Sign Up
*/
get_header();
?>
<div class="signup-container">
<div class="signup-content">
<h1><?php esc_html_e('Sign Up for Updates', 'bd-murphy'); ?></h1>
<p class="signup-description"><?php esc_html_e('Stay updated with new releases, newsletters, and exclusive content.', 'bd-murphy'); ?></p>
<?php if (isset($_GET['success'])) : ?>
<div class="signup-success">
<p><?php esc_html_e('Thank you for signing up! Check your email to confirm your subscription.', 'bd-murphy'); ?></p>
</div>
<?php else : ?>
<form id="signup-form" class="signup-form" method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<?php wp_nonce_field('bdm_signup', 'bdm_signup_nonce'); ?>
<input type="hidden" name="action" value="bdm_signup_submit">
<p class="form-field">
<label for="email"><?php esc_html_e('Email Address', 'bd-murphy'); ?> *</label>
<input type="email" id="email" name="email" required>
</p>
<p class="form-field">
<label for="first_name"><?php esc_html_e('First Name', 'bd-murphy'); ?></label>
<input type="text" id="first_name" name="first_name">
</p>
<p class="form-field checkbox-field">
<input type="checkbox" id="opt-in" name="opt_in" value="1" required>
<label for="opt-in"><?php esc_html_e('I agree to receive email updates from the author.', 'bd-murphy'); ?></label>
</p>
<p class="form-submit">
<button type="submit" class="submit-btn"><?php esc_html_e('Subscribe', 'bd-murphy'); ?></button>
</p>
</form>
<?php endif; ?>
</div>
</div>
<?php
get_footer();

View File

@@ -0,0 +1,38 @@
<?php
/**
* Template Name: Unsubscribe
*/
get_header();
?>
<div class="unsubscribe-container">
<div class="unsubscribe-content">
<h1><?php esc_html_e('Unsubscribe', 'bd-murphy'); ?></h1>
<?php if (isset($_GET['success'])) : ?>
<div class="unsubscribe-success">
<p><?php esc_html_e('You have been successfully unsubscribed.', 'bd-murphy'); ?></p>
</div>
<?php else : ?>
<p><?php esc_html_e('Enter your email address to unsubscribe from the mailing list.', 'bd-murphy'); ?></p>
<form id="unsubscribe-form" class="unsubscribe-form" method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<?php wp_nonce_field('bdm_unsubscribe', 'bdm_unsubscribe_nonce'); ?>
<input type="hidden" name="action" value="bdm_unsubscribe_submit">
<p class="form-field">
<label for="email"><?php esc_html_e('Email Address', 'bd-murphy'); ?></label>
<input type="email" id="email" name="email" required>
</p>
<p class="form-submit">
<button type="submit" class="submit-btn"><?php esc_html_e('Unsubscribe', 'bd-murphy'); ?></button>
</p>
</form>
<?php endif; ?>
</div>
</div>
<?php
get_footer();

View File

@@ -0,0 +1,42 @@
<?php
/**
* Search Results Template
*/
get_header();
?>
<div class="search-container">
<header class="page-header">
<h1 class="page-title">
<?php
printf(esc_html__('Search Results for: %s', 'bd-murphy'), '<span>' . get_search_query() . '</span>');
?>
</h1>
</header>
<div class="search-results">
<?php
if (have_posts()) :
while (have_posts()) :
the_post();
?>
<article id="post-<?php the_ID(); ?>" <?php post_class('search-entry'); ?>>
<h2 class="search-entry-title">
<a href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
</h2>
<div class="search-entry-excerpt">
<?php the_excerpt(); ?>
</div>
</article>
<?php
endwhile;
else :
?>
<p class="no-results"><?php esc_html_e('No results found. Please try a different search.', 'bd-murphy'); ?></p>
<?php endif; ?>
</div>
</div>
<?php
get_footer();

View File

@@ -0,0 +1,129 @@
<?php
/**
* Template Name: Individual Book
*/
get_header();
?>
<div class="book-page-container">
<?php
while (have_posts()) :
the_post();
$book_id = get_the_ID();
$languages = BD_BookShelfHelper::get_supported_languages();
$current_lang = isset($_GET['lang']) ? sanitize_text_field($_GET['lang']) : 'en';
$title = BD_BookShelfHelper::get_localized_title($book_id, $current_lang);
$blurb = BD_BookShelfHelper::get_localized_blurb($book_id, $current_lang);
$cover_url = BD_BookShelfHelper::get_cover_url($book_id, 'book-page-cover', $current_lang);
$author = BD_BookShelfHelper::get_pen_name($book_id);
$series = BD_BookShelfHelper::get_series_info($book_id);
$ratings = BD_BookShelfHelper::get_ratings($book_id);
$preview = BD_BookShelfHelper::get_preview_excerpt($book_id, $current_lang);
$pull_quotes = BD_BookShelfHelper::get_pull_quotes($book_id, $current_lang);
$reviews = BD_BookShelfHelper::get_reviews($book_id);
$short_url = bdm_get_short_url($book_id);
?>
<nav class="language-selector">
<?php foreach ($languages as $code => $name) : ?>
<a href="?lang=<?php echo esc_attr($code); ?>"
class="lang-btn <?php echo $current_lang === $code ? 'active' : ''; ?>"
data-lang="<?php echo esc_attr($code); ?>">
<?php echo esc_html($name); ?>
</a>
<?php endforeach; ?>
</nav>
<div class="book-page-content">
<div class="book-page-cover">
<img src="<?php echo esc_url($cover_url); ?>" alt="<?php echo esc_attr($title); ?>">
</div>
<div class="book-page-info">
<h1 class="book-title"><?php echo esc_html($title); ?></h1>
<p class="book-pen-name"><?php echo esc_html($author); ?></p>
<?php if ($series) : ?>
<p class="book-series-info">
<a href="<?php echo esc_url(home_url('/series/' . $series['slug'])); ?>">
<?php printf(esc_html__('Part of the %s series — Book %d', 'bd-murphy'), esc_html($series['name']), esc_html($series['volume'])); ?>
</a>
</p>
<?php endif; ?>
<?php if ($ratings) : ?>
<div class="book-ratings">
<?php if (!empty($ratings['amazon'])) : ?>
<span class="rating amazon">Amazon: <?php echo esc_html($ratings['amazon']); ?></span>
<?php endif; ?>
<?php if (!empty($ratings['goodreads'])) : ?>
<span class="rating goodreads">Goodreads: <?php echo esc_html($ratings['goodreads']); ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="book-format-toggle">
<button class="format-btn active" data-format="ebook"><?php esc_html_e('Ebook', 'bd-murphy'); ?></button>
<button class="format-btn" data-format="paperback"><?php esc_html_e('Paperback', 'bd-murphy'); ?></button>
<button class="format-btn" data-format="hardback"><?php esc_html_e('Hardback', 'bd-murphy'); ?></button>
</div>
<div class="book-retailer-links">
<?php BD_BookShelfHelper::display_retailer_links($book_id, 'ebook'); ?>
</div>
<div class="book-preview">
<h3><?php esc_html_e('Read Preview', 'bd-murphy'); ?></h3>
<p><?php echo esc_html($preview); ?></p>
</div>
<?php if ($pull_quotes) : ?>
<div class="book-pull-quotes">
<?php foreach ($pull_quotes as $quote) : ?>
<blockquote class="pull-quote"><?php echo esc_html($quote); ?></blockquote>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($reviews) : ?>
<div class="book-reviews">
<h3><?php esc_html_e('Reviews', 'bd-murphy'); ?></h3>
<?php foreach ($reviews as $review) : ?>
<div class="review-card">
<p class="review-text"><?php echo esc_html($review->comment_text); ?></p>
<cite class="review-source">
<?php if ($review->source_url) : ?>
<a href="<?php echo esc_url($review->source_url); ?>" target="_blank" rel="noopener">
<?php echo esc_html($review->source_name); ?>
</a>
<?php else : ?>
<?php echo esc_html($review->source_name); ?>
<?php endif; ?>
</cite>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="book-blurb">
<h3><?php esc_html_e('About This Book', 'bd-murphy'); ?></h3>
<p><?php echo esc_html($blurb); ?></p>
</div>
<button class="share-btn" data-url="<?php echo esc_url($short_url); ?>">
<?php esc_html_e('Share', 'bd-murphy'); ?>
</button>
<div class="social-share">
<?php get_template_part('template-parts/social-share'); ?>
</div>
</div>
</div>
<?php
endwhile;
?>
</div>
<?php
get_footer();

View File

@@ -0,0 +1,18 @@
/*
Theme Name: BDMurphy-BookShelf-Theme
Author: B.D. Murphy
Author URI: https://authorbdmurphy.com
Description: Custom WordPress theme for B.D. Murphy's author website featuring book showcase, series management, blog, and reviews.
Version: 1.5.0
Requires at least: 6.0
Tested up to: 6.4
Requires PHP: 7.4
Text Domain: bd-murphy
Domain Path: /languages
License: GPL v2 or later
*/
(function() {
'use strict';
document.documentElement.className = document.documentElement.className.replace(/\bno-js\b/, '') + ' js';
})();

View File

@@ -0,0 +1,68 @@
<?php
/**
* Template Name: Individual Series
*/
get_header();
?>
<div class="single-series-container">
<?php
while (have_posts()) :
the_post();
$series_id = get_the_ID();
$header_url = BD_BookShelfHelper::get_series_header_url($series_id, 'series-header');
$name = get_the_title();
$blurb = BD_BookShelfHelper::get_series_blurb($series_id);
$books = BD_BookShelfHelper::get_series_books($series_id);
?>
<div class="series-header">
<img src="<?php echo esc_url($header_url); ?>" alt="<?php echo esc_attr($name); ?>" class="series-header-image">
</div>
<div class="series-info">
<h1 class="series-name"><?php echo esc_html($name); ?></h1>
<p class="series-blurb"><?php echo esc_html($blurb); ?></p>
</div>
<nav class="breadcrumb">
<a href="<?php echo esc_url(home_url('/')); ?>"><?php esc_html_e('Home', 'bd-murphy'); ?></a> &rsaquo;
<a href="<?php echo esc_url(home_url('/series/')); ?>"><?php esc_html_e('Series', 'bd-murphy'); ?></a> &rsaquo;
<span class="current"><?php echo esc_html($name); ?></span>
</nav>
<div class="series-books">
<?php if ($books) : ?>
<?php foreach ($books as $book) : ?>
<?php
$cover_url = BD_BookShelfHelper::get_cover_url($book->id, 'book-cover');
$title = esc_html($book->title);
$author = BD_BookShelfHelper::get_pen_name($book->id);
$volume = $book->volume_number;
$blurb_excerpt = BD_BookShelfHelper::get_blurb_excerpt($book->id);
?>
<article class="series-book-entry">
<div class="series-book-cover">
<img src="<?php echo esc_url($cover_url); ?>" alt="<?php echo $title; ?>">
</div>
<div class="series-book-info">
<span class="volume-label"><?php printf(esc_html__('Book %d', 'bd-murphy'), esc_html($volume)); ?></span>
<h2 class="series-book-title"><?php echo $title; ?></h2>
<p class="series-book-author"><?php echo esc_html($author); ?></p>
<p class="series-book-blurb"><?php echo esc_html($blurb_excerpt); ?></p>
<?php BD_BookShelfHelper::display_retailer_links($book->id); ?>
</div>
</article>
<?php endforeach; ?>
<?php else : ?>
<p class="no-books-in-series"><?php esc_html_e('No books in this series yet.', 'bd-murphy'); ?></p>
<?php endif; ?>
</div>
<?php
endwhile;
?>
</div>
<?php
get_footer();

View File

@@ -0,0 +1,53 @@
<?php
/**
* Social Share Buttons Template Part
*/
if (!defined('ABSPATH')) {
exit;
}
$share_url = isset($short_url) ? $short_url : get_permalink();
$share_title = get_the_title();
$show_facebook = get_theme_mod('share_facebook', true);
$show_twitter = get_theme_mod('share_twitter', true);
$show_linkedin = get_theme_mod('share_linkedin', true);
$show_whatsapp = get_theme_mod('share_whatsapp', true);
?>
<div class="social-share-buttons">
<span class="share-label"><?php esc_html_e('Share:', 'bd-murphy'); ?></span>
<?php if ($show_facebook) : ?>
<a href="https://www.facebook.com/sharer/sharer.php?u=<?php echo urlencode($share_url); ?>"
class="share-btn share-facebook" target="_blank" rel="noopener noreferrer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
</a>
<?php endif; ?>
<?php if ($show_twitter) : ?>
<a href="https://twitter.com/intent/tweet?url=<?php echo urlencode($share_url); ?>&text=<?php echo urlencode($share_title); ?>"
class="share-btn share-twitter" target="_blank" rel="noopener noreferrer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
</a>
<?php endif; ?>
<?php if ($show_linkedin) : ?>
<a href="https://www.linkedin.com/shareArticle?mini=true&url=<?php echo urlencode($share_url); ?>&title=<?php echo urlencode($share_title); ?>"
class="share-btn share-linkedin" target="_blank" rel="noopener noreferrer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.678 2.678 0 01-2.643-2.68 2.68 2.68 0 112.68 2.68zm1.5 13.019H3.823V9h2.014v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
</a>
<?php endif; ?>
<?php if ($show_whatsapp) : ?>
<a href="https://wa.me/?text=<?php echo urlencode($share_title . ' ' . $share_url); ?>"
class="share-btn share-whatsapp" target="_blank" rel="noopener noreferrer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.119-1.473-.425-.517-.71-.954-.793-1.115-.082-.161.008-.33.103-.435.098-.098.218-.253.335-.382.113-.125.201-.227.297-.378.098-.151.174-.252.248-.252.071 0 .177-.035.37-.164.196-.129.325-.29.44-.469.119-.18.21-.378.297-.567.088-.188.176-.387.251-.579.078-.193.04-.356-.018-.498-.058-.147-.267-.352-.535-.537-.256-.177-.456-.313-.655-.499-.197-.185-.37-.409-.52-.634-.15-.227-.126-.419-.035-.652.092-.231.419-.493.576-.663.152-.166.31-.347.44-.534.133-.187.22-.31.298-.388.076-.076.14-.127.2-.17.058-.042.11-.066.165-.079.052-.013.108-.01.165-.003.057.008.117.035.204.088.086.052.147.139.201.238.054.098.108.209.156.323.049.114.088.198.132.264.044.065.078.105.117.138.039.033.085.05.14.06l.163.03c.102.012.203-.009.328-.077.124-.067.247-.185.366-.32.117-.136.225-.307.307-.493.081-.186.125-.339.159-.46.034-.12.038-.21.03-.27-.007-.06-.032-.098-.06-.124-.026-.027-.059-.046-.083-.062-.024-.016-.042-.031-.059-.043-.015-.012-.026-.018-.036-.021-.01-.002-.015-.003-.021-.001zM12 0C5.373 0 0 5.373 0 12c0 2.108.554 4.074 1.511 5.78L.789 23.172l5.463-1.433c1.596.88 3.432 1.379 5.348 1.379 6.627 0 12-5.373 12-12S18.627 0 12 0zm5.073 11.225c-.051.14-.373.648-.818.915.476.259.918.416 1.226.473.305.057.646.068.951.017.309-.051.508-.094.704-.165.195-.072.416-.36.565-.617.146-.258.216-.48.243-.635.026-.156.024-.356-.102-.734-.125-.379-.261-.523-.395-.623-.134-.1-.278-.14-.405-.092-.127.049-.545.255-.988.856-.444.601-.741 1.34-.82 1.56-.08.222-.03.338.063.446.094.108.214.275.314.413.097.139.197.296.271.414.072.118.12.221.163.303.042.082.073.123.092.153.019.03.026.049.034.067.008.018.01.035-.002.058-.013.023-.04.095-.107.193z"/></svg>
</a>
<?php endif; ?>
<button class="share-btn share-copy" data-url="<?php echo esc_url($share_url); ?>">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
</button>
</div>

BIN
Old/bd-simpleemail.zip Executable file

Binary file not shown.

View File

@@ -0,0 +1,33 @@
<?php
/**
* Plugin Name: BD SimpleEmail
* Plugin URI: https://authorbdmurphy.com
* Description: Email newsletter management with AI-assisted composition using Gemini API and subscriber management.
* Version: 1.5.0
* Author: B.D. Murphy
* Author URI: https://authorbdmurphy.com
* License: GPL v2 or later
* Text Domain: bd-simpleemail
*/
if (!defined('ABSPATH')) {
exit;
}
define('BD_EMAIL_VERSION', '1.5.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';
function BD_SimpleEmail() {
return BD_SimpleEmail::instance();
}
add_action('plugins_loaded', 'BD_SimpleEmail');
register_activation_hook(__FILE__, array('BD_SimpleEmail_Install', 'activate'));
register_deactivation_hook(__FILE__, array('BD_SimpleEmail_Install', 'deactivate'));

View File

@@ -0,0 +1,311 @@
<?php
/**
* Email Admin Class
*/
if (!defined('ABSPATH')) {
exit;
}
class BD_SimpleEmail_Admin {
private static $instance = null;
public static function instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
add_action('admin_enqueue_scripts', array($this, 'enqueue_assets'));
}
public function enqueue_assets($hook) {
$screen = get_current_screen();
if (strpos($hook, 'bd-simpleemail') !== false || strpos($hook, 'bd-email') !== false) {
wp_enqueue_style('bd-email-admin', BD_EMAIL_URI . 'assets/admin.css', array(), BD_EMAIL_VERSION);
wp_enqueue_script('bd-email-admin', BD_EMAIL_URI . 'assets/admin.js', array('jquery'), BD_EMAIL_VERSION, true);
}
}
public static function subscribers_page() {
global $wpdb;
$table_name = $wpdb->prefix . 'bd_subscribers';
if (isset($_POST['bd_export_subscribers'])) {
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename=subscribers.csv');
$subscribers = $wpdb->get_results("SELECT email, first_name, status, subscribed_at FROM {$table_name} WHERE status = 'subscribed'");
echo 'Email,First Name,Status,Subscribed Date' . "\n";
foreach ($subscribers as $sub) {
echo esc_csv($sub->email) . ',' . esc_csv($sub->first_name) . ',' . esc_csv($sub->status) . ',' . esc_csv($sub->subscribed_at) . "\n";
}
exit;
}
$subscribers = $wpdb->get_results("SELECT * FROM {$table_name} ORDER BY subscribed_at DESC");
$total = count($subscribers);
$subscribed = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name} WHERE status = 'subscribed'");
$unsubscribed = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name} WHERE status = 'unsubscribed'");
echo '<div class="wrap">';
echo '<h1>' . esc_html__('Subscribers', 'bd-simpleemail') . '</h1>';
echo '<div class="bd-email-stats">';
echo '<div class="stat-box"><h2>' . esc_html($total) . '</h2><p>Total</p></div>';
echo '<div class="stat-box"><h2>' . esc_html($subscribed) . '</h2><p>Active</p></div>';
echo '<div class="stat-box"><h2>' . esc_html($unsubscribed) . '</h2><p>Unsubscribed</p></div>';
echo '</div>';
echo '<form method="post">';
echo '<p><input type="submit" name="bd_export_subscribers" class="button" value="' . esc_attr__('Export CSV', 'bd-simpleemail') . '"></p>';
echo '</form>';
echo '<table class="widefat">';
echo '<thead><tr><th>ID</th><th>Email</th><th>First Name</th><th>Status</th><th>Subscribed</th><th>Unsubscribed</th></tr></thead>';
echo '<tbody>';
foreach ($subscribers as $sub) {
echo '<tr>';
echo '<td>' . esc_html($sub->id) . '</td>';
echo '<td>' . esc_html($sub->email) . '</td>';
echo '<td>' . esc_html($sub->first_name) . '</td>';
echo '<td>' . esc_html($sub->status) . '</td>';
echo '<td>' . esc_html($sub->subscribed_at) . '</td>';
echo '<td>' . ($sub->unsubscribed_at ? esc_html($sub->unsubscribed_at) : '-') . '</td>';
echo '</tr>';
}
echo '</tbody></table></div>';
}
public static function new_newsletter_page() {
$error = '';
$success = '';
if (isset($_POST['bd_send_newsletter']) && check_admin_referer('bd_send_newsletter')) {
$subject = sanitize_text_field($_POST['subject']);
$content = wp_kses_post($_POST['content']);
$ai_generate = isset($_POST['ai_generate']);
$publish_blog = isset($_POST['publish_blog']);
if ($ai_generate && $subject) {
$prompt = sanitize_textarea_field($_POST['ai_prompt']);
$content = self::generate_newsletter($subject, $prompt);
}
if ($content) {
$result = BD_Email_Sender::send_newsletter($subject, $content);
if ($result['success']) {
self::log_newsletter($subject, $content, $result['count']);
$success = sprintf(__('Newsletter sent to %d subscribers!', 'bd-simpleemail'), $result['count']);
if ($publish_blog) {
self::publish_as_blog_post($subject, $content);
}
} else {
$error = $result['message'];
}
} else {
$error = __('Content is required', 'bd-simpleemail');
}
}
if ($ai_settings = BD_SimpleEmail::get_settings()) {
$has_ai = !empty($ai_settings['gemini_api_key']);
} else {
$has_ai = false;
}
echo '<div class="wrap">';
echo '<h1>' . esc_html__('New Newsletter', 'bd-simpleemail') . '</h1>';
if ($error) {
echo '<div class="error"><p>' . esc_html($error) . '</p></div>';
}
if ($success) {
echo '<div class="updated"><p>' . esc_html($success) . '</p></div>';
}
if (!$has_ai) {
echo '<div class="notice notice-warning"><p>' . esc_html__('Gemini API key not configured. AI generation disabled.', 'bd-simpleemail') . '</p></div>';
}
echo '<form method="post" id="newsletter-form">';
wp_nonce_field('bd_send_newsletter');
echo '<table class="form-table">';
echo '<tr><th>' . esc_html__('Subject', 'bd-simpleemail') . '</th>';
echo '<td><input type="text" name="subject" class="large-text" required></td></tr>';
if ($has_ai) {
echo '<tr><th>' . esc_html__('AI Generate', 'bd-simpleemail') . '</th>';
echo '<td><input type="checkbox" name="ai_generate" id="ai_generate" value="1"> <label for="ai_generate">' . esc_html__('Use Gemini AI to generate content', 'bd-simpleemail') . '</label></td></tr>';
echo '<tr class="ai-prompt-row" style="display:none;"><th>' . esc_html__('Prompt', 'bd-simpleemail') . '</th>';
echo '<td><textarea name="ai_prompt" rows="4" class="large-text" placeholder="Describe what the newsletter should contain..."></textarea></td></tr>';
}
echo '<tr><th>' . esc_html__('Content', 'bd-simpleemail') . '</th>';
echo '<td><textarea name="content" rows="20" class="large-text" id="newsletter-content"></textarea></td></tr>';
echo '<tr><th>' . esc_html__('Options', 'bd-simpleemail') . '</th>';
echo '<td><input type="checkbox" name="publish_blog" id="publish_blog" value="1"> <label for="publish_blog">' . esc_html__('Also publish as blog post', 'bd-simpleemail') . '</label></td></tr>';
echo '</table>';
echo '<p><input type="submit" name="bd_send_newsletter" class="button button-primary" value="' . esc_attr__('Send Newsletter', 'bd-simpleemail') . '"></p>';
echo '</form>';
echo '<script>';
echo "jQuery(document).ready(function($) {";
echo "$('#ai_generate').on('change', function() {";
echo "$('.ai-prompt-row').toggle(this.checked);";
echo "});";
echo "});";
echo '</script>';
echo '</div>';
}
public static function settings_page() {
if (isset($_POST['bd_save_email_settings'])) {
update_option('bdm_gmail_address', sanitize_email($_POST['gmail_address']));
update_option('bdm_sender_name', sanitize_text_field($_POST['sender_name']));
update_option('bdm_reply_to', sanitize_email($_POST['reply_to']));
if (!empty($_POST['gmail_password'])) {
update_option('bdm_gmail_password', BD_Email_Sender::encrypt_password($_POST['gmail_password']));
}
if (!empty($_POST['gemini_api_key'])) {
update_option('bdm_gemini_api_key', sanitize_text_field($_POST['gemini_api_key']));
}
echo '<div class="updated"><p>' . esc_html__('Settings saved.', 'bd-simpleemail') . '</p></div>';
}
$settings = BD_SimpleEmail::get_settings();
$business = BD_SimpleEmail::get_business_info();
echo '<div class="wrap">';
echo '<h1>' . esc_html__('BD SimpleEmail Settings', 'bd-simpleemail') . '</h1>';
echo '<form method="post">';
wp_nonce_field('bd_email_settings');
echo '<h2>' . esc_html__('Gmail Configuration', 'bd-simpleemail') . '</h2>';
echo '<p>' . esc_html__('Configure your Gmail account for sending newsletters via SMTP.', 'bd-simpleemail') . '</p>';
echo '<table class="form-table">';
echo '<tr><th>' . esc_html__('Gmail Address', 'bd-simpleemail') . '</th>';
echo '<td><input type="email" name="gmail_address" value="' . esc_attr($settings['gmail_address']) . '" class="regular-text"></td></tr>';
echo '<tr><th>' . esc_html__('App Password', 'bd-simpleemail') . '</th>';
echo '<td><input type="password" name="gmail_password" class="regular-text" placeholder="Leave blank to keep current">';
echo '<p class="description"><a href="https://support.google.com/accounts/answer/185833" target="_blank">' . esc_html__('Learn how to generate an app password', 'bd-simpleemail') . '</a></p></td></tr>';
echo '<tr><th>' . esc_html__('Sender Name', 'bd-simpleemail') . '</th>';
echo '<td><input type="text" name="sender_name" value="' . esc_attr($settings['sender_name']) . '" class="regular-text"></td></tr>';
echo '<tr><th>' . esc_html__('Reply-To', 'bd-simpleemail') . '</th>';
echo '<td><input type="email" name="reply_to" value="' . esc_attr($settings['reply_to']) . '" class="regular-text"></td></tr>';
echo '</table>';
echo '<h2>' . esc_html__('Gemini AI', 'bd-simpleemail') . '</h2>';
echo '<table class="form-table">';
echo '<tr><th>' . esc_html__('API Key', 'bd-simpleemail') . '</th>';
echo '<td><input type="text" name="gemini_api_key" value="' . esc_attr($settings['gemini_api_key']) . '" class="regular-text">';
echo '<p class="description"><a href="https://makersuite.google.com/app/apikey" target="_blank">' . esc_html__('Get your API key', 'bd-simpleemail') . '</a></p></td></tr>';
echo '</table>';
echo '<h2>' . esc_html__('Business Information (CAN-SPAM)', 'bd-simpleemail') . '</h2>';
echo '<p>' . esc_html__('These fields are required for CAN-SPAM compliance and appear in email footers.', 'bd-simpleemail') . '</p>';
echo '<table class="form-table">';
echo '<tr><th>' . esc_html__('Business Name', 'bd-simpleemail') . '</th>';
echo '<td>' . esc_html($business['name'] ?: '-') . '</td></tr>';
echo '<tr><th>' . esc_html__('Address', 'bd-simpleemail') . '</th>';
echo '<td>' . esc_html($business['address_1']) . '<br>' . esc_html($business['address_2']) . '<br>' . esc_html($business['city']) . '<br>' . esc_html($business['country']) . '</td></tr>';
echo '<tr><th>' . esc_html__('Phone', 'bd-simpleemail') . '</th>';
echo '<td>' . esc_html($business['phone'] ?: '-') . '</td></tr>';
echo '</table>';
echo '<p><input type="submit" name="bd_save_email_settings" class="button button-primary" value="' . esc_attr__('Save Settings', 'bd-simpleemail') . '"></p>';
echo '</form></div>';
}
private static function generate_newsletter($subject, $prompt) {
$settings = BD_SimpleEmail::get_settings();
if (empty($settings['gemini_api_key'])) {
return '';
}
$system_instruction = "You are a professional newsletter writer for an author. Write engaging, concise newsletter content in HTML format. Use <p> for paragraphs, <h2> for headings, and <ul> for lists. Keep it friendly but professional.";
$full_prompt = $system_instruction . "\n\nSubject: " . $subject . "\n\nAdditional instructions: " . $prompt;
$api_url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=' . $settings['gemini_api_key'];
$body = array(
'contents' => array(
array(
'parts' => array(
array('text' => $full_prompt)
)
)
),
'generationConfig' => array(
'temperature' => 0.9,
'maxOutputTokens' => 2048,
),
);
$response = wp_remote_post($api_url, array(
'body' => json_encode($body),
'headers' => array(
'Content-Type' => 'application/json',
),
));
if (is_wp_error($response)) {
return '';
}
$data = json_decode(wp_remote_retrieve_body($response), true);
if (isset($data['candidates'][0]['content']['parts'][0]['text'])) {
return $data['candidates'][0]['content']['parts'][0]['text'];
}
return '';
}
private static function log_newsletter($subject, $content, $recipient_count) {
global $wpdb;
$table_name = $wpdb->prefix . 'bd_newsletters';
$wpdb->insert($table_name, array(
'subject' => $subject,
'body_html' => $content,
'sent_at' => current_time('mysql'),
'recipient_count' => $recipient_count,
));
}
private static function publish_as_blog_post($subject, $content) {
$post_id = wp_insert_post(array(
'post_title' => $subject,
'post_content' => $content,
'post_status' => 'publish',
'post_type' => 'post',
));
return $post_id;
}
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* Email Installation and Database
*/
if (!defined('ABSPATH')) {
exit;
}
class BD_SimpleEmail_Install {
public static function activate() {
self::create_tables();
}
public static function deactivate() {
}
public static function create_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$tables = array(
"CREATE TABLE IF NOT EXISTS {$wpdb->prefix}bd_subscribers (
id bigint(20) NOT NULL AUTO_INCREMENT,
email varchar(255) NOT NULL,
first_name varchar(100) DEFAULT NULL,
status varchar(20) DEFAULT 'subscribed',
subscribed_at datetime DEFAULT CURRENT_TIMESTAMP,
unsubscribed_at datetime DEFAULT NULL,
ip_address varchar(45) DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY email (email)
) $charset_collate;",
"CREATE TABLE IF NOT EXISTS {$wpdb->prefix}bd_newsletters (
id bigint(20) NOT NULL AUTO_INCREMENT,
subject varchar(255) NOT NULL,
body_html text,
sent_at datetime DEFAULT CURRENT_TIMESTAMP,
recipient_count int(11) DEFAULT 0,
PRIMARY KEY (id)
) $charset_collate;",
);
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
foreach ($tables as $table) {
dbDelta($table);
}
}
}

View File

@@ -0,0 +1,162 @@
<?php
/**
* Email Sender Class
*/
if (!defined('ABSPATH')) {
exit;
}
class BD_Email_Sender {
public static function send_newsletter($subject, $content) {
$settings = BD_SimpleEmail::get_settings();
if (empty($settings['gmail_address']) || empty($settings['gmail_password'])) {
return array(
'success' => false,
'message' => __('Gmail not configured', 'bd-simpleemail'),
);
}
$subscribers = BD_SimpleEmail::get_subscribers();
if (empty($subscribers)) {
return array(
'success' => false,
'message' => __('No subscribers found', 'bd-simpleemail'),
);
}
$business = BD_SimpleEmail::get_business_info();
$html = self::build_email_html($subject, $content, $business);
$sent = 0;
foreach ($subscribers as $subscriber) {
$result = self::send_email(
$subscriber->email,
$subject,
$html,
$subscriber->unsubscribe_token
);
if ($result) {
$sent++;
}
}
return array(
'success' => true,
'count' => $sent,
);
}
private static function send_email($to, $subject, $html, $unsubscribe_token = '') {
$settings = BD_SimpleEmail::get_settings();
$headers = array(
'Content-Type: text/html; charset=UTF-8',
'From: ' . $settings['sender_name'] . ' <' . $settings['gmail_address'] . '>',
);
if (!empty($settings['reply_to'])) {
$headers[] = 'Reply-To: ' . $settings['reply_to'];
}
$headers[] = 'MIME-Version: 1.0';
return wp_mail($to, $subject, $html, $headers);
}
private static function build_email_html($subject, $content, $business) {
$site_url = get_bloginfo('url');
$site_name = get_bloginfo('name');
$unsubscribe_link = add_query_arg(array(
'unsubscribe' => '{{email}}',
'token' => '{{token}}',
), home_url('/unsubscribe/'));
$address_html = '';
if ($business['name']) {
$address_html = '<p>' . esc_html($business['name']) . '<br>';
if ($business['address_1']) {
$address_html .= esc_html($business['address_1']) . '<br>';
}
if ($business['address_2']) {
$address_html .= esc_html($business['address_2']) . '<br>';
}
if ($business['city']) {
$address_html .= esc_html($business['city']) . '<br>';
}
if ($business['country']) {
$address_html .= esc_html($business['country']);
}
$address_html .= '</p>';
}
$html = '
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>' . esc_html($subject) . '</title>
<style>
body { margin: 0; padding: 0; font-family: Georgia, serif; font-size: 16px; line-height: 1.6; color: #2C2C2C; background-color: #F5F0E8; }
.email-container { max-width: 600px; margin: 0 auto; background-color: #ffffff; }
.email-header { background-color: #1B3A6B; padding: 30px; text-align: center; }
.email-header h1 { color: #C8A96E; margin: 0; font-size: 28px; }
.email-body { padding: 30px; }
.email-footer { background-color: #EDE4D0; padding: 20px 30px; text-align: center; font-size: 12px; color: #555555; }
a { color: #2E6DA4; }
.unsubscribe-link { color: #888888; text-decoration: underline; }
</style>
</head>
<body>
<div class="email-container">
<div class="email-header">
<h1>' . esc_html($site_name) . '</h1>
</div>
<div class="email-body">
' . $content . '
</div>
<div class="email-footer">
<p>
<a href="' . esc_url($site_url) . '">' . esc_html($site_url) . '</a>
</p>
' . $address_html . '
<p>
<a href="' . esc_url($unsubscribe_link) . '">Unsubscribe</a> from this mailing list.
</p>
<p>You received this email because you signed up for updates from ' . esc_html($site_name) . '.</p>
</div>
</div>
</body>
</html>
';
return $html;
}
public static function encrypt_password($password) {
$key = defined('AUTH_KEY') ? AUTH_KEY : 'bdm_email_secret';
$iv = openssl_random_pseudo_bytes(16);
$encrypted = openssl_encrypt($password, 'AES-256-CBC', $key, 0, $iv);
return base64_encode($iv . $encrypted);
}
public static function decrypt_password($encrypted) {
$data = base64_decode($encrypted);
$key = defined('AUTH_KEY') ? AUTH_KEY : 'bdm_email_secret';
$iv = substr($data, 0, 16);
$ciphertext = substr($data, 16);
return openssl_decrypt($ciphertext, 'AES-256-CBC', $key, 0, $iv);
}
}

View File

@@ -0,0 +1,278 @@
<?php
/**
* Main SimpleEmail Plugin Class
*/
if (!defined('ABSPATH')) {
exit;
}
class BD_SimpleEmail {
private static $instance = null;
public static function instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->init_hooks();
}
private function init_hooks() {
add_action('admin_menu', array($this, 'add_menu_pages'));
add_action('admin_post_bdm_signup_submit', array($this, 'handle_signup'));
add_action('admin_post_bdm_unsubscribe_submit', array($this, 'handle_unsubscribe'));
add_action('wp_ajax_bdm_ajax_signup', array($this, 'ajax_signup'));
add_action('wp_ajax_nopriv_bdm_ajax_signup', array($this, 'ajax_signup'));
add_shortcode('bdm_signup', array($this, 'signup_shortcode'));
}
public function add_menu_pages() {
add_menu_page(
__('BD SimpleEmail', 'bd-simpleemail'),
__('BD SimpleEmail', 'bd-simpleemail'),
'manage_options',
'bd-simpleemail',
'',
'dashicons-email',
32
);
add_submenu_page(
'bd-simpleemail',
__('Subscribers', 'bd-simpleemail'),
__('Subscribers', 'bd-simpleemail'),
'manage_options',
'bd-email-subscribers',
array('BD_SimpleEmail_Admin', 'subscribers_page')
);
add_submenu_page(
'bd-simpleemail',
__('New Newsletter', 'bd-simpleemail'),
__('New Newsletter', 'bd-simpleemail'),
'manage_options',
'bd-email-new',
array('BD_SimpleEmail_Admin', 'new_newsletter_page')
);
add_submenu_page(
'bd-simpleemail',
__('Settings', 'bd-simpleemail'),
__('Settings', 'bd-simpleemail'),
'manage_options',
'bd-email-settings',
array('BD_SimpleEmail_Admin', 'settings_page')
);
}
public function handle_signup() {
if (!isset($_POST['bdm_signup_nonce']) || !wp_verify_nonce($_POST['bdm_signup_nonce'], 'bdm_signup')) {
wp_safe_redirect(add_query_arg('error', 'invalid', home_url('/signup/')));
exit;
}
$email = sanitize_email($_POST['email']);
$first_name = sanitize_text_field($_POST['first_name']);
if (!$email) {
wp_safe_redirect(add_query_arg('error', 'invalid', home_url('/signup/')));
exit;
}
$result = self::subscribe($email, $first_name);
if ($result) {
wp_safe_redirect(add_query_arg('success', '1', home_url('/signup/')));
} else {
wp_safe_redirect(add_query_arg('error', 'exists', home_url('/signup/')));
}
exit;
}
public function handle_unsubscribe() {
if (!isset($_POST['bdm_unsubscribe_nonce']) || !wp_verify_nonce($_POST['bdm_unsubscribe_nonce'], 'bdm_unsubscribe')) {
wp_safe_redirect(add_query_arg('error', 'invalid', home_url('/unsubscribe/')));
exit;
}
$email = sanitize_email($_POST['email']);
if ($email) {
self::unsubscribe($email);
}
wp_safe_redirect(add_query_arg('success', '1', home_url('/unsubscribe/')));
exit;
}
public function ajax_signup() {
check_ajax_referer('bdm_ajax_signup');
$email = sanitize_email($_POST['email']);
if (!$email || !is_email($email)) {
wp_send_json_error(array('message' => 'Invalid email address'));
}
$first_name = isset($_POST['first_name']) ? sanitize_text_field($_POST['first_name']) : '';
$opt_in = isset($_POST['opt_in']) && $_POST['opt_in'];
if (!$opt_in) {
wp_send_json_error(array('message' => 'Please confirm your subscription'));
}
$result = self::subscribe($email, $first_name);
if ($result) {
wp_send_json_success(array('message' => 'Thank you for subscribing!'));
} else {
wp_send_json_error(array('message' => 'You are already subscribed or cannot be added'));
}
}
public static function subscribe($email, $first_name = '') {
global $wpdb;
$table_name = $wpdb->prefix . 'bd_subscribers';
$exists = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$table_name} WHERE email = %s",
$email
));
if ($exists) {
return false;
}
$result = $wpdb->insert($table_name, array(
'email' => $email,
'first_name' => $first_name,
'status' => 'subscribed',
'subscribed_at' => current_time('mysql'),
'ip_address' => $_SERVER['REMOTE_ADDR'],
));
return $result;
}
public static function unsubscribe($email) {
global $wpdb;
$table_name = $wpdb->prefix . 'bd_subscribers';
return $wpdb->update($table_name, array(
'status' => 'unsubscribed',
'unsubscribed_at' => current_time('mysql'),
), array('email' => $email));
}
public static function get_subscribers($status = 'subscribed') {
global $wpdb;
$table_name = $wpdb->prefix . 'bd_subscribers';
return $wpdb->get_results($wpdb->prepare(
"SELECT * FROM {$table_name} WHERE status = %s ORDER BY subscribed_at DESC",
$status
));
}
public static function get_subscriber_count($status = 'subscribed') {
global $wpdb;
$table_name = $wpdb->prefix . 'bd_subscribers';
return $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$table_name} WHERE status = %s",
$status
));
}
public static function get_settings() {
return array(
'gmail_address' => get_option('bdm_gmail_address', ''),
'gmail_password' => get_option('bdm_gmail_password', ''),
'sender_name' => get_option('bdm_sender_name', get_bloginfo('name')),
'reply_to' => get_option('bdm_reply_to', ''),
'gemini_api_key' => get_option('bdm_gemini_api_key', ''),
);
}
public static function get_business_info() {
$options = get_option('bdm_theme_options', array());
return array(
'name' => isset($options['business_name']) ? $options['business_name'] : '',
'address_1' => isset($options['business_address_1']) ? $options['business_address_1'] : '',
'address_2' => isset($options['business_address_2']) ? $options['business_address_2'] : '',
'city' => isset($options['business_city']) ? $options['business_city'] : '',
'country' => isset($options['business_country']) ? $options['business_country'] : '',
'phone' => isset($options['business_phone']) ? $options['business_phone'] : '',
);
}
public static function generate_unsubscribe_token($email) {
$secret_key = defined('AUTH_KEY') ? AUTH_KEY : 'bdm_unsubscribe_secret';
return hash_hmac('sha256', $email, $secret_key);
}
public static function verify_unsubscribe_token($email, $token) {
return hash_equals(self::generate_unsubscribe_token($email), $token);
}
public function signup_shortcode($atts = array()) {
$atts = shortcode_atts(array(), $atts);
ob_start();
?>
<form id="bdm-signup-form" class="bdm-signup-form">
<?php wp_nonce_field('bdm_ajax_signup', 'bdm_signup_nonce'); ?>
<p class="form-field">
<label for="bdm-email"><?php esc_html_e('Email Address', 'bd-simpleemail'); ?> *</label>
<input type="email" id="bdm-email" name="email" required>
</p>
<p class="form-field">
<label for="bdm-first-name"><?php esc_html_e('First Name', 'bd-simpleemail'); ?></label>
<input type="text" id="bdm-first-name" name="first_name">
</p>
<p class="form-field checkbox-field">
<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>
</p>
<p class="form-submit">
<button type="submit" class="submit-btn"><?php esc_html_e('Subscribe', 'bd-simpleemail'); ?></button>
</p>
<div class="form-response" style="display:none;"></div>
</form>
<script>
jQuery(document).ready(function($) {
$('#bdm-signup-form').on('submit', function(e) {
e.preventDefault();
var form = $(this);
var response = form.find('.form-response');
$.ajax({
type: 'POST',
url: '<?php echo admin_url('admin-ajax.php'); ?>',
data: form.serialize() + '&action=bdm_ajax_signup',
success: function(response) {
if (response.success) {
response.html('<p class="success">' + response.data.message + '</p>').show();
form[0].reset();
} else {
response.html('<p class="error">' + response.data.message + '</p>').show();
}
},
error: function() {
response.html('<p class="error"><?php esc_html_e('An error occurred. Please try again.', 'bd-simpleemail'); ?></p>').show();
}
});
});
});
</script>
<?php
return ob_get_clean();
}
}

BIN
Old/bdmurphy-theme.zip Executable file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@@ -0,0 +1,439 @@
================================================================================
BD BOOKHELPER PLUGIN SPECIFICATION (Book Data Management)
================================================================================
Version: 2.0.0
Location: /wp-content/plugins/bd-bookshelfhelper/
================================================================================
1. OVERVIEW
================================================================================
This plugin is the core book data management system. It handles all aspects of
storing, retrieving, and displaying book information including titles, blurbs,
covers, ratings, reviews, and retailer links. It is designed to work
seamlessly with the BD BookShelf theme to provide an Amazon-style book display.
================================================================================
2. DUPLICATE INFORMATION FROM BD BOOKSHELF PLUGIN
================================================================================
For complete plugin details, see SPEC_BD_Bookshelf_Plugin.txt.
This specification duplicates key information for reference.
================================================================================
3. SHORTCODES FOR BOOK DISPLAY
================================================================================
The plugin provides two main shortcodes for displaying books:
[bd-book book="ID|TITLE"]
- Displays a single book by ID or title
- Uses render_book_display() for HTML output
[bd-books limit="-1" orderby="date" order="DESC"]
- Displays all books sorted by publish date (newest first)
- Default: Shows all books in DESC order by date
- Output wrapped in <div class="bd-books-list">
USAGE IN THEME:
Home page: [bd-books limit="3"] (shows 3 most recent books)
Books page: [bd-books limit="-1"] (shows all books)
================================================================================
4. AMAZON DETAIL LAYOUT OUTPUT
================================================================================
The plugin generates this specific HTML structure for the Amazon Detail layout:
<div class="bd-book-listing">
<div class="bd-book-entry">
<!-- LEFT COLUMN (25%) -->
<div class="bd-book-cover">
<a href="[book-url]">
<img src="[cover-url]" alt="[book-title] Cover">
</a>
<div class="bd-book-purchase">
<div class="bd-book-format-selector">
<button class="bd-book-format-btn active" data-format="ebook">eBook</button>
<button class="bd-book-format-btn" data-format="paperback">Paperback</button>
<button class="bd-book-format-btn" data-format="hardback">Hardback</button>
</div>
<div class="bd-book-retailers" data-book-id="[book-id]">
[retailer links with retailer-box class]
</div>
</div>
</div>
<!-- RIGHT COLUMN (75%) -->
<div class="bd-book-details">
<h2 class="bd-book-title">
<a href="[book-url]">[book-title]</a>
</h2>
<p class="bd-book-byline">by [pen-name]</p>
<div class="bd-book-ratings">
<span class="rating-amazon">Amazon: [rating]/5</span>
<span class="rating-goodreads">Goodreads: [rating]/5</span>
</div>
<hr class="bd-book-separator">
<div class="bd-book-blurb">
[localized blurb with wpautop()]
</div>
<div class="bd-book-metadata">
<span class="metadata-genre">[genre]</span> |
<span class="metadata-pages">[pages] pages</span> |
<span class="metadata-language">[language]</span> |
<span class="metadata-publisher">[publisher]</span>
</div>
<div class="bd-book-categories">[category-list]</div>
</div>
</div>
</div>
================================================================================
5. RETAILER LINKS (retailer-box styling)
================================================================================
Output via display_retailer_links() method:
```html
<a href="[url]" class="retailer-box retailer-[key]" target="_blank" rel="noopener noreferrer">[name]</a>
```
CLASSES:
retailer-box Base class (shared styling)
retailer-amazon Amazon - background: #FF9900
retailer-apple Apple Books - background: #b3b3b3
retailer-barnes Barnes & Noble - background: #8b0000
retailer-kobo Kobo - background: #f4f4f4
retailer-google Google Play - background: #ffffff
STYLING (matching Goodreads widget style):
.retailer-box {
display: inline-block;
padding: 10px 15px;
text-align: center;
border: 1px solid #dee2e6;
border-radius: 4px;
text-decoration: none;
font-weight: 500;
}
Unavailable retailers get: .retailer-unavailable (grayed out)
================================================================================
6. DATABASE SCHEMA (COMPLETE)
================================================================================
CREATE TABLE {prefix}bd_books (
id BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
post_id BIGINT(20) NOT NULL UNIQUE,
pen_name_id BIGINT(20),
series_id BIGINT(20),
volume_number INT(11),
asin VARCHAR(20),
isbn VARCHAR(20),
publish_date DATE,
sort_order INT(11) DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE {prefix}bd_book_meta (
id BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
book_id BIGINT(20) NOT NULL,
language_code VARCHAR(5) NOT NULL,
title VARCHAR(255),
blurb TEXT,
preview_excerpt TEXT,
pull_quotes JSON,
UNIQUE KEY book_language (book_id, language_code)
);
CREATE TABLE {prefix}bd_pen_names (
id BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
pen_name VARCHAR(255) NOT NULL,
notes TEXT,
active TINYINT(1) DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE {prefix}bd_reviews (
id BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
book_id BIGINT(20) NOT NULL,
comment_text TEXT,
synopsis TEXT,
source_name VARCHAR(255) NOT NULL,
source_url VARCHAR(500),
display_order INT(11) DEFAULT 0
);
CREATE TABLE {prefix}bd_ratings (
id BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
book_id BIGINT(20) NOT NULL,
source VARCHAR(20) NOT NULL,
rating DECIMAL(3,2),
review_count INT(11),
last_updated DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY book_source (book_id, source)
);
CREATE TABLE {prefix}bd_series (
id BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE,
cover_attachment_id BIGINT(20),
header_attachment_id BIGINT(20),
blurb TEXT,
display_order INT(11) DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE {prefix}bd_review_shortlinks (
id BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY,
book_id BIGINT(20) NOT NULL,
site_code VARCHAR(5) NOT NULL,
slug VARCHAR(20) NOT NULL UNIQUE,
destination_url VARCHAR(500),
status VARCHAR(20) DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
activated_at DATETIME
);
================================================================================
7. POST META FIELDS
================================================================================
The plugin stores metadata on 'book' post type:
IDENTIFICATION:
pen_name_id int FK to bd_pen_names
asin string Amazon ASIN (10 chars)
isbn string ISBN-13
short_link_slug string Short URL slug
SERIES & ORDER:
volume_number int Position in series
publish_date date Publication date
sort_order int Manual sort
RATINGS:
amazon_rating string e.g., "4.5"
goodreads_rating string e.g., "4.2"
LOCALIZED CONTENT (en, fr, de, es, it, pt, ru, ja, zh):
title_{lang} Title in language
blurb_{lang} Description in language
preview_excerpt_{lang} Preview text
pull_quote_{lang}_{n} Pull quotes 1-5
genre_tags_{lang} Genre tags
amazon_search_terms_{lang} Amazon keywords
cover_{lang} Cover image ID
RETAILER LINKS:
{format}_url_{retailer}
Format keys: ebook, paperback, hardback, audiobook
Retailer keys: amazon, apple, barnes, kobo, google, bookshop,
amazon_uk, waterstones, audible, scribner
EXAMPLES:
ebook_url_amazon
paperback_url_barnes
hardback_url_amazon
audiobook_url_audible
================================================================================
8. HELPER METHODS FOR THEME USE
================================================================================
BD_BookShelfHelper::get_cover_url($book_id, $size = 'thumbnail', $lang = 'en')
Returns cover image URL with language fallback
BD_BookShelfHelper::get_ratings($book_id)
Returns: array('amazon' => '4.5', 'goodreads' => '4.2')
BD_BookShelfHelper::get_retailer_links($book_id, $format = 'ebook')
Returns array of retailer link data
BD_BookShelfHelper::display_retailer_links($book_id, $format = 'ebook')
Echos retailer links with retailer-box classes
BD_BookShelfHelper::get_localized_title($book_id, $lang = 'en')
Gets title in specified language (falls back to English)
BD_BookShelfHelper::get_localized_blurb($book_id, $lang = 'en')
Gets blurb in specified language
BD_BookShelfHelper::get_pen_name($book_id)
Gets author pen name
BD_BookShelfHelper::get_series_info($book_id)
Returns: array('id', 'name', 'slug', 'volume')
BD_BookShelfHelper::get_supported_languages()
Returns: array('en'=>'English', 'fr'=>'Francais', etc.)
================================================================================
9. MULTI-LANGUAGE IMPLEMENTATION
================================================================================
Supported Languages (9):
en, fr, de, es, it, pt, ru, ja, zh
Data Storage:
- Each language stores its own meta field: title_en, title_fr, blurb_en, etc.
- get_localized_blurb($book_id, 'fr') returns blurb_fr meta
- Falls back to English if requested language not set
Admin Interface:
- Language tabs in data entry form
- Each language panel contains: title, blurb, preview, pull quotes, genre, search terms
================================================================================
10. CSS CLASSES OUTPUT BY PLUGIN
================================================================================
FRONTEND (theme must style these):
.bd-book-listing Outer container
.bd-book-entry Flex container
.bd-book-cover Left column (25%)
.bd-book-purchase Purchase section
.bd-book-format-selector Format buttons container
.bd-book-format-btn Format buttons
.bd-book-format-btn.active Active format
.bd-book-retailers Retailer links container
.bd-book-details Right column (75%)
.bd-book-title Title heading
.bd-book-byline "by Author" line
.bd-book-ratings Ratings container
.rating-amazon Amazon rating
.rating-goodreads Goodreads rating
.bd-book-separator HR divider
.bd-book-blurb Description
.bd-book-metadata Metadata row
.metadata-genre Genre span
.metadata-pages Pages span
.metadata-language Language span
.metadata-publisher Publisher span
.bd-book-categories Categories
.bd-books-list Multiple books wrapper
.bd-book-error Error message
RETAILER LINKS:
.retailer-box Base class
.retailer-amazon Amazon styling
.retailer-apple Apple Books styling
.retailer-barnes B&N styling
.retailer-kobo Kobo styling
.retailer-google Google Play styling
.retailer-unavailable Disabled/grayed out
================================================================================
11. REQUIRED CSS VARIABLES (from theme)
================================================================================
The plugin expects these CSS variables to be defined by the theme:
:root {
--bdm-navy: #1B3A6B; /* Primary color - headers, nav */
--bdm-slate: #2E6DA4; /* Hover states */
--bdm-sky: #4A90D9; /* Links on hover */
--bdm-gold: #C8A96E; /* Accent - highlights, borders */
--bdm-cream: #F5F0E8; /* Page background */
--bdm-parchment: #EDE4D0; /* Alternate background */
--bdm-body-text: #2C2C2C; /* Primary text */
--bdm-mid-grey: #555555; /* Secondary text, bylines */
--bdm-light-grey: #888888; /* Metadata text */
--bdm-active-green: #2D6A4F; /* Active/positive states */
--bdm-heading-font: "Playfair Display", Georgia, serif;
--bdm-body-font: "Lato", Calibri, sans-serif;
--bdm-card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
--bdm-card-shadow-hover: 0 8px 24px rgba(0, 0, 0, 0.12);
--bdm-border-radius: 8px;
--bdm-transition: all 0.3s ease;
}
================================================================================
12. CUSTOM POST TYPES & TAXONOMIES
================================================================================
CPT: book
Public: Yes
Has Archive: Yes (/book)
REST API: Yes
Supports: title, editor, thumbnail, excerpt, custom-fields
Taxonomy: series
Hierarchical: Yes
Object Type: book
REST API: Yes
================================================================================
13. ADMIN FEATURES
================================================================================
Admin Pages (under BD BookHelper menu):
- Dashboard: Overview stats
- Book Data Entry: Multi-language form interface
- Series: Taxonomy management
- Short Links: URL slug management
- Reviews: Review management
- Pen Names: Author name CRUD
- Settings: Affiliate codes, Goodreads ID
- Migrate: From BD Books plugin
Meta Boxes on book posts:
- Book Details: ASIN, ISBN, publish date, series, volume
- Book SEO: Custom title, description
================================================================================
14. INTEGRATION WITH THEME
================================================================================
Nav Integration:
Hooks into 'bdm_nav_menu_items' filter to add Series dropdown
Adds My Books submenu with all book titles
Adds Series submenu with series taxonomy terms
Short Link Handling:
Registers rewrite rule for /[slug]/ pattern
Redirects to book when short link accessed
Schema.org Data:
generate_schema() creates JSON-LD structured data
output_seo_tags() outputs Open Graph and meta tags
================================================================================
15. ACTIVATION REQUIREMENTS
================================================================================
On activation, creates:
- Database tables
- book_manager role (limited capabilities)
- Default pen name "B.D. Murphy"
- Scheduled cron for Goodreads/Amazon updates
On deactivation:
- Removes book_manager role
- Clears cron events
- Flushes rewrite rules
================================================================================
16. AJAX HANDLERS
================================================================================
wp_ajax_bdm_autosave - Auto-save form data
wp_ajax_bdm_scan_retailers - Scan retailer websites
wp_ajax_bdm_update_links - Update retailer links
All require:
- check_ajax_referer() verification
- current_user_can('edit_posts') capability check
================================================================================

View File

@@ -0,0 +1,552 @@
================================================================================
BD BOOKSHELF PLUGIN SPECIFICATION
================================================================================
Version: 2.2.0
Location: /wp-content/plugins/bd-bookshelf/
================================================================================
1. OVERVIEW
================================================================================
This plugin serves as a helper for BD Bookshelf. it is a plugin with the same name to show they go together. Make your website the destination to find the best place to buy your book. Enter your ISBN and the system will search book seller websites for your book.
The plugin will let the user enter a book by Title and ISBNs. There can be four ISBNs for each book, coorisponding to the form factor: eBook, Paperback, Hard Cover, audio. The system will allow entry and track this information.
With the ISBN the system will use known patters to get direct links to popular book selling sites. The user can see the links and edit them.
The sites are Amazon, Barnes and Noble, Apple, Google, Kobo.
The plugin will allow the user to create a new link, entering the name and the link manually. The icon for these links will be a general page of a book and quill.
The user can enter affiliate codes related to each of the sites and the system will automatically incorporate them into links to that site.
The system will use the ISBN/ASIN information to get ratings for the book. It will get the information from Amazon and Goodreads (author information - author ID in theme). The system will scan and do updates every 12 hours with a cron job. The ratings will be shown when the book is displayed with a [rating] short tag - specific to that book.
The plugin will create a grid of selling site icons and links when a short code is used on a page. The short codes are specific to format and ISBN. codes: [sell_ebook], [sell_pb], [sell_hc], [sell_audio]
The plugin will provide the popup and checks for GDPR compliance
The plugin will allow creating and editing SEO metadata for book pages.
The plugin will allow creation and management of short codes. A code defined by website/shrtcode that can be redirected, expanded to another site.
GDPR compliance - get approval for cookies.
7) SEO metadata
8) short codes
2) User enters book
Title and ISBN - a book can have eBook, Paperback, Hardcover, Audio.
2.1) site identifier ASIN/ISBN
3) System confirms top sites name and link pattern - show site general link search on sites - Need ISBN/ASIN/ident
3.1 list of sites with pattern to search.
Amazon
B&N
Apple
Google
Kobo
4) Adding affiliate codes for sites
5)get ratings from site (Amazon/goodreads) - limit updates to every x hours
6) Generate sell buttons/links from a shortcode - links with affiliate codes on page.
codes: [sell_ebook], [sell_pb], [sell_hc], [sell_audio] When the code is entered the system will generate a grid of the site options to purchase the book.
6.2)
PlatformPrimary IdentifierBest URL Structure for Programmatic Linking
Bookshop.org ISBN-13 https://bookshop.org/books/{ISBN13}
Barnes & Noble (Print)EAN / ISBN-13 https://www.barnesandnoble.com/s/{ISBN13}
shortcodes?
"Theme Transition Helper" - it provides a bridge
between WordPress and the BD BookShelf theme by managing book data, generating
the Amazon-style book display layout, and providing shortcodes for theme use.
Purpose:
- Manage book data (titles, blurbs, covers, ratings) in custom database tables
- Generate HTML output with specific CSS classes (.bd-book-*) for theme styling
- Provide REST API endpoints for book data
- Handle multi-language support with localized content per language
- Manage retailer links and affiliate tracking
================================================================================
2. FILE STRUCTURE
================================================================================
bd-bookshelfhelper.php Main plugin file - initialization, constants
inc/class-bookhelper.php Core plugin class - CPTs, taxonomies, shortcodes
inc/class-admin.php Admin functionality - data entry interface
inc/class-install.php Database installation, table creation
assets/css/admin.css Admin interface styling
assets/js/admin.js Enhanced data entry with auto-save/validation
uninstall.php Cleanup on plugin deletion
================================================================================
3. CUSTOM POST TYPES
================================================================================
CPT: "book"
---------------------------------------------------------------------------
Public: Yes
Has Archive: Yes (/book)
Show in Menu: No
Supports: title, editor, thumbnail, excerpt, custom-fields
REST API: Yes (show_in_rest = true)
Menu Icon: dashicons-book-alt
Capabilities: Uses native post capability type (edit_posts, etc.)
CPT: "bd_shortlink"
---------------------------------------------------------------------------
Purpose: Internal short link management
Public: No
Show UI: No
================================================================================
4. CUSTOM TAXONOMIES
================================================================================
Taxonomy: "series" (Hierarchical)
---------------------------------------------------------------------------
Object Type: book
Public: Yes
Show Admin Column: Yes
REST API: Yes
Rewrite: /series
================================================================================
5. DATABASE TABLES
================================================================================
TABLE: {prefix}bd_books
---------------------------------------------------------------------------
id BIGINT(20) AUTO_INCREMENT PRIMARY KEY
post_id BIGINT(20) NOT NULL UNIQUE
pen_name_id BIGINT(20)
series_id BIGINT(20)
volume_number INT(11)
asin VARCHAR(20)
isbn VARCHAR(20)
publish_date DATE
sort_order INT(11) DEFAULT 0
created_at DATETIME
TABLE: {prefix}bd_book_meta
---------------------------------------------------------------------------
id BIGINT(20) AUTO_INCREMENT PRIMARY KEY
book_id BIGINT(20) NOT NULL
language_code VARCHAR(5) NOT NULL
title VARCHAR(255)
blurb TEXT
preview_excerpt TEXT
pull_quotes JSON
UNIQUE KEY (book_id, language_code)
TABLE: {prefix}bd_pen_names
---------------------------------------------------------------------------
id BIGINT(20) AUTO_INCREMENT PRIMARY KEY
pen_name VARCHAR(255) NOT NULL
notes TEXT
active TINYINT(1) DEFAULT 1
created_at DATETIME
TABLE: {prefix}bd_reviews
---------------------------------------------------------------------------
id BIGINT(20) AUTO_INCREMENT PRIMARY KEY
book_id BIGINT(20) NOT NULL
comment_text TEXT
synopsis TEXT
source_name VARCHAR(255) NOT NULL
source_url VARCHAR(500)
display_order INT(11) DEFAULT 0
TABLE: {prefix}bd_ratings
---------------------------------------------------------------------------
id BIGINT(20) AUTO_INCREMENT PRIMARY KEY
book_id BIGINT(20) NOT NULL
source VARCHAR(20) NOT NULL
rating DECIMAL(3,2)
review_count INT(11)
last_updated DATETIME
UNIQUE KEY (book_id, source)
TABLE: {prefix}bd_series
---------------------------------------------------------------------------
id BIGINT(20) AUTO_INCREMENT PRIMARY KEY
name VARCHAR(255) NOT NULL
slug VARCHAR(100) NOT NULL UNIQUE
cover_attachment_id BIGINT(20)
header_attachment_id BIGINT(20)
blurb TEXT
display_order INT(11) DEFAULT 0
created_at DATETIME
TABLE: {prefix}bd_review_shortlinks
---------------------------------------------------------------------------
id BIGINT(20) AUTO_INCREMENT PRIMARY KEY
book_id BIGINT(20) NOT NULL
site_code VARCHAR(5) NOT NULL
slug VARCHAR(20) NOT NULL UNIQUE
destination_url VARCHAR(500)
status VARCHAR(20) DEFAULT 'pending'
created_at DATETIME
activated_at DATETIME
================================================================================
6. POST META FIELDS FOR BOOKS
================================================================================
Identification:
pen_name_id int FK to bd_pen_names table
asin string Amazon Standard Identification Number (10 chars)
isbn string ISBN-13 (13 digits)
short_link_slug string Custom short URL slug (e.g., "ph")
Series & Order:
volume_number int Book position in series
publish_date date Publication date (YYYY-MM-DD)
sort_order int Manual sort position
Ratings:
amazon_rating string Amazon rating (e.g., "4.5")
goodreads_rating string Goodreads rating (e.g., "4.2")
Localized Content (by language code: en, fr, de, es, it, pt, ru, ja, zh):
title_{lang} string Book title in language
blurb_{lang} string Back cover blurb
preview_excerpt_{lang} string Preview text excerpt
pull_quote_{lang}_{n} string Pull quote n (1-5)
genre_tags_{lang} string Genre tags (comma-separated)
amazon_search_terms_{lang} string Amazon search keywords
cover_{lang} int Cover image ID for language
Retailer Links (format + retailer):
{format}_url_{retailer}
Formats: ebook, paperback, hardback, audiobook
Retailers: amazon, apple, barnes, kobo, google, bookshop, amazon_uk, waterstones, audible, scribner
Examples:
ebook_url_amazon eBook Amazon link
paperback_url_barnes B&N paperback link
hardback_url_amazon Amazon hardback link
SEO:
seo_title string Custom meta title
seo_description string Meta description
Legacy/Migration:
genre string Primary genre
pages string Page count
language string Book language (default: "English")
publisher string Publisher name
================================================================================
7. SHORTCODES
================================================================================
SHORTCODE: [bd-book]
---------------------------------------------------------------------------
Displays a single book by ID or title.
Attributes:
book (string, required) - Book ID (integer) or title
Usage:
[bd-book book="123"]
[bd-book book="Pandemic Hacker"]
Output: Returns render_book_display() HTML or error message with bd-book-error class.
SHORTCODE: [bd-books]
---------------------------------------------------------------------------
Displays all books in a list.
Attributes:
limit int default: -1 (all) - Number of books to display
orderby string default: "date" - Sort field (date, title, menu_order, etc.)
order string default: "DESC" - Sort direction (ASC or DESC)
Usage:
[bd-books limit="10" orderby="date" order="DESC"]
Output: Wraps all render_book_display() outputs in <div class="bd-books-list">
================================================================================
8. RENDER_BOOK_DISPLAY() OUTPUT STRUCTURE
================================================================================
This is the CORE HTML OUTPUT that the plugin generates. The theme MUST provide
CSS styling for these classes to achieve the Amazon Detail layout.
```html
<div class="bd-book-listing">
<div class="bd-book-entry">
<!-- LEFT COLUMN (25%) -->
<div class="bd-book-cover">
<a href="[book-url]">
<img src="[cover-url]" alt="[book-title] Cover">
</a>
<div class="bd-book-purchase">
<div class="bd-book-format-selector">
<button class="bd-book-format-btn active" data-format="ebook">eBook</button>
<button class="bd-book-format-btn" data-format="paperback">Paperback</button>
<button class="bd-book-format-btn" data-format="hardback">Hardback</button>
</div>
<div class="bd-book-retailers" data-book-id="[book-id]">
<!-- Retailer links injected here by display_retailer_links() -->
</div>
</div>
</div>
<!-- RIGHT COLUMN (75%) -->
<div class="bd-book-details">
<h2 class="bd-book-title">
<a href="[book-url]">[book-title]</a>
</h2>
<p class="bd-book-byline">by [pen-name]</p>
<div class="bd-book-ratings">
<span class="rating-amazon">Amazon: [rating]/5</span>
<span class="rating-goodreads">Goodreads: [rating]/5</span>
</div>
<hr class="bd-book-separator">
<div class="bd-book-blurb">
[blurb content with wpautop()]
</div>
<div class="bd-book-metadata">
<span class="metadata-genre">[genre]</span> |
<span class="metadata-pages">[pages] pages</span> |
<span class="metadata-language">[language]</span> |
<span class="metadata-publisher">[publisher]</span>
</div>
<div class="bd-book-categories">
[category-list]
</div>
</div>
</div>
</div>
```
================================================================================
9. DISPLAY_RETAILER_LINKS() METHOD
================================================================================
Method: public static function display_retailer_links($book_id, $format = 'ebook')
Output HTML:
```html
<a href="[url]" class="retailer-box retailer-[key]" target="_blank" rel="noopener noreferrer">[name]</a>
```
Supported Retailers:
Key Name Base URL
---- ------------- ----------------------------------------------
amazon Amazon https://www.amazon.com/dp/
apple Apple Books https://books.apple.com/
barnes Barnes & Noble https://www.barnesandnoble.com/
kobo Kobo https://www.kobo.com/
google Google Play https://play.google.com/store/apps/details?id=
Amazon Auto-URL Generation:
If asin meta is set but no URL, constructs: https://www.amazon.com/dp/{asin}?tag={affiliate}
================================================================================
10. CSS CLASSES GENERATED BY PLUGIN
================================================================================
The following classes are output by the plugin and MUST be styled by the theme:
FRONTEND BOOK DISPLAY:
bd-book-listing Outer wrapper container
bd-book-entry Flex container with cover + details
bd-book-cover Left column (25% width)
bd-book-purchase Purchase section below cover
bd-book-format-selector Container for format toggle buttons
bd-book-format-btn Individual format button
bd-book-format-btn.active Active format state
bd-book-retailers Container for retailer-box links
bd-book-details Right column (75% width)
bd-book-title Title heading (h2)
bd-book-title a Title link
bd-book-byline "by [pen-name]" text
bd-book-ratings Ratings container
rating-amazon Amazon rating span
rating-goodreads Goodreads rating span
bd-book-separator HR element (divider)
bd-book-blurb Book description/synopsis
bd-book-metadata Genre, pages, language, publisher row
metadata-genre Genre span
metadata-pages Pages span
metadata-language Language span
metadata-publisher Publisher span
bd-book-categories Book categories
bd-book-error Error message paragraph
BOOKS LIST WRAPPER:
bd-books-list Container wrapping multiple book listings
RETAILER LINKS:
retailer-box Base class for all retailer links
retailer-amazon Amazon links
retailer-apple Apple Books links
retailer-barnes Barnes & Noble links
retailer-kobo Kobo links
retailer-google Google Play links
retailer-unavailable Disabled/unavailable retailer
================================================================================
11. STATIC HELPER METHODS
================================================================================
The following static methods are available for theme use:
get_featured_books($limit = 5) Get books ordered by publish_date
get_cover_url($book_id, $size = 'thumbnail', $lang = 'en') Get cover image URL
get_book_url($book_id) Get book URL (short link or permalink)
get_ratings($book_id) Get amazon/goodreads ratings array
get_retailer_links($book_id, $format = 'ebook') Get retailer purchase links array
get_localized_title($book_id, $lang = 'en') Get title for language
get_localized_blurb($book_id, $lang = 'en') Get blurb for language
get_preview_excerpt($book_id, $lang = 'en') Get preview excerpt
get_pull_quotes($book_id, $lang = 'en') Get up to 5 pull quotes
get_reviews($book_id) Get reviews from bd_reviews table
get_pen_name($book_id) Get author pen name
get_series_info($book_id) Get series data + volume
series_count() Count of active series
get_supported_languages() List supported language codes
get_awards() Get awards option
output_seo_tags($book_id) Output meta/OG tags
generate_schema($book_id) Generate schema.org data
================================================================================
12. MULTI-LANGUAGE SUPPORT
================================================================================
Supported Languages:
en English
fr Francais
de Deutsch
es Espanol
it Italiano
pt Portugues
ru Russkiy
ja Japanese
zh Chinese (Simplified)
Implementation:
- Each language gets its own meta fields (title_en, title_fr, blurb_en, etc.)
- get_localized_title/blurb methods default to English if requested language not found
- Data entry interface has language tabs with panels for each language
================================================================================
13. CRON JOBS
================================================================================
Hook: bd_bookshelf_goodreads_refresh
Schedule: Daily
Purpose: Refresh data from Goodreads API
Hook: bd_bookshelf_amazon_scrape
Schedule: Every 6 hours
Purpose: Scrape Amazon for ratings
================================================================================
14. THEME DEPENDENCIES
================================================================================
The plugin is designed to work with the BD BookShelf theme.
Integration points:
1. Nav Menu Filter: Hooks into bdm_nav_menu_items filter to inject Series menu
2. Book Display: render_book_display() outputs HTML with .bd-book-* classes
3. Cover Images: Uses theme's standard WordPress featured image system
4. Short Link Rewrite: Registers /[slug] pattern that works site-wide
================================================================================
15. ADMIN PAGES
================================================================================
Menu: BD BookHelper (main menu icon: dashicons-book-alt)
Dashboard bd-bookshelf-dashboard Overview and quick links
Books edit.php?post_type=book Book list
Book Data Entry bd-bookshelf-data-entry Enhanced book editor
Series edit-tags.php?taxonomy=series Series taxonomy management
Short Links bd-bookshelf-bulk-actions Short link management
Reviews bd-bookshelf-reviews Review management
Pen Names bd-bookshelf-pen-names Pen name CRUD
Settings bd-bookshelf-settings Affiliate codes, Goodreads ID
Migrate bd-bookshelf-migrate Migration from BD Books
================================================================================
16. REQUIRED THEME CSS (MINIMUM)
================================================================================
The theme MUST provide CSS for the following classes to render the Amazon Detail layout:
/* Book Listing Container */
.bd-book-listing { margin-bottom: 2rem; }
/* Entry Flex Container */
.bd-book-entry { display: flex; gap: 2rem; }
/* Cover Column (25%) */
.bd-book-cover { flex: 0 0 25%; max-width: 25%; }
.bd-book-cover img { width: 100%; height: auto; box-shadow: 0 4px 8px rgba(0,0,0,0.2); border-radius: 4px; }
/* Format Selector */
.bd-book-format-selector { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
.bd-book-format-btn { padding: 0.5rem 1rem; border: 1px solid var(--bdm-navy); background: #fff; cursor: pointer; border-radius: 4px; }
.bd-book-format-btn.active { background: var(--bdm-navy); color: #fff; }
/* Retailer Links */
.bd-book-retailers { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.retailer-box { display: inline-block; padding: 0.5rem 1rem; background: #FF9900; color: #000; text-decoration: none; border-radius: 4px; font-weight: bold; }
/* Details Column (75%) */
.bd-book-details { flex: 1; }
.bd-book-title { margin: 0 0 0.5rem 0; font-family: var(--bdm-heading-font); font-size: 1.5rem; }
.bd-book-byline { color: var(--bdm-mid-grey); margin: 0 0 1rem 0; }
.bd-book-ratings { margin-bottom: 1rem; }
.bd-book-separator { border: none; border-top: 1px solid #eee; margin: 1rem 0; }
.bd-book-blurb { margin-bottom: 1rem; line-height: 1.8; }
.bd-book-metadata { margin-bottom: 1rem; color: var(--bdm-mid-grey); }
.bd-book-categories { font-size: 0.85rem; color: var(--bdm-light-grey); }
/* Responsive */
@media (max-width: 768px) {
.bd-book-entry { flex-direction: column; }
.bd-book-cover { flex: 0 0 100%; max-width: 100%; }
}
================================================================================
17. ACTIVATION/DEACTIVATION
================================================================================
Activation creates:
- All database tables via dbDelta()
- 'book_manager' role with limited capabilities
- Default pen name "B.D. Murphy"
- Scheduled cron events
- Flushes rewrite rules
Deactivation removes:
- 'book_manager' role
- Scheduled cron events
- Flushes rewrite rules
================================================================================

View File

@@ -0,0 +1,186 @@
# BD Bookshelf Plugin - Questions for Review
Please review these questions and provide answers. Each answer will be incorporated into the final specification.
---
## 1. Data Architecture
**Q1.1:** Should books be stored as a **Custom Post Type (CPT)** (leveraging WordPress's native metadata, SEO plugins, and editor) or as **Custom Database Tables** (better performance for large catalogs but requires more custom UI)?
A: Use a custom database table - it will be leveraged later with another plugin
**Q1.2:** How should the plugin connect to the Author ID from the theme? Options:
- (a) Auto-detect from the current logged-in user
- (b) Manual selection per book from a user dropdown
- (c) Inherited from a global plugin setting
A: If the plugin can't read the author ID, goodread id from the theme configuration data it should have a setting menu item to enter "Author IDs"
---
## 2. ISBN & Link Generation
**Q2.1:** Do we have a documented list of the exact URL patterns for each retail site (Amazon, B&N, Apple, Google, Kobo), or should the plugin attempt to "discover" them via API?
A: This will be a seperate search and analysis. The pattern will be provided in a specification update.
**Q2.2:** For Amazon, should we use the Product Advertising API (requires API keys) or scrape the product page for ratings?
A: Scrape the product page. This does not require the author to have an account for advertising.
**Q2.3:** Should custom retail sites (the "book and quill" icon sites) support affiliate codes, or only the predefined sites?
A: affiliate codes only for the predefined sites.
---
## 3. Rating Synchronization
**Q3.1:** For Amazon ratings, should the plugin:
- (a) Scrape the product page (no API key needed, but fragile)
- (b) Use the Product Advertising API (requires Amazon affiliate account and API keys)
- (c) Use a third-party service
A: a robust scraping of the website
**Q3.2:** For Goodreads ratings, should the plugin:
- (a) Scrape the Goodreads page (no API key, but fragile and potentially rate-limited)
- (b) Use the Goodreads API (deprecated, no longer available to new apps)
- (c) Use a third-party service
a: a robust scraping of the website.
**Q3.3:** What should happen if a rating fetch fails? Options:
- (a) Keep the last known rating indefinitely
- (b) Clear the rating after X failed attempts
- (c) Mark as "unavailable" and stop retrying
A: If the fetch fails keep the last rating received. If there is no previously saved rating show zero.
---
## 4. Shortcode Behavior
**Q4.1:** For the `[rating]` shortcode, how should it know which book to display if not on a specific book page? Options:
- (a) Require an ID parameter: `[rating id="123"]`
- (b) Use a title parameter: `[rating title="Pandemic Hacker"]`
- (c) Only work on book CPT pages (context-aware)
A: a) require the title. The user doesn't need to look up or remember codes
**Q4.2:** For the `[sell_*]` shortcodes, should they:
- (a) Only work on book CPT pages (auto-detect the book)
- (b) Require an ID parameter: `[sell_ebook id="123"]`
- (c) Support both options
A: The destination for a short code can be any website. Even another website. Local site isn't required.
**Q4.3:** For custom shortcodes, what constitutes an "expansion"? Options:
- (a) Simple 301/302/307 redirect
- (b) Render the target URL as a clickable link
- (c) Fetch and display the remote content (proxy)
A: redirects - including permanent
---
## 5. SEO Metadata
**Q5.1:** Should the SEO module:
- (a) Only manage on-page meta tags (title, description, keywords)
- (b) Also generate structured data (JSON-LD for books)
- (c) Integrate with existing SEO plugins (Yoast, RankMath) via their API
A: The preference is to integrate with other plugins. The minimum is to provide meta data tags. The stretch goal is to have links to other refernences (facebook, etc) to enhance search engine results.
**Q5.2:** Should the plugin generate a sitemap entry for each book, or rely on existing SEO plugins?
A: don't rely on other plugins.
---
## 6. GDPR Compliance
**Q6.1:** Should the plugin provide default privacy policy text, or just the popup mechanism?
A: Provide an example, common from the internet text.
**Q6.2:** What data does the plugin collect that requires GDPR consent? Options:
- (a) Cookie for affiliate tracking
- (b) Click tracking for shortcodes
- (c) Both
A: a)
**Q6.3:** Should the consent popup be:
- (a) A simple banner at the bottom of the page
- (b) A modal popup that blocks interaction until accepted
- (c) Configurable by the admin
A: a)
---
## 7. Affiliate Codes
**Q7.1:** Should affiliate codes be:
- (a) Global (same code for all books on a site)
- (b) Per-site (different codes for Amazon vs B&N)
- (c) Per-book (different codes for different books on the same site)
A: Each site could have a different affiliate code - even if they are the same, each site should have an affiliate code entry.
**Q7.2:** What query parameter name does each site use for affiliate codes? Examples:
- Amazon: `?tag=`
- B&N: `?ean=`
- Others: ?
A: This will be researched and provided later
---
## 8. Custom Shortcode Manager
**Q8.1:** For the shortcode manager, what is the primary use case? Options:
- (a) URL shortening for marketing (e.g., `site.com/go/my-promo`)
- (b) Affiliate link cloaking
- (c) Both
A: a) Book sites do NOT allow links in a book to a competitor. Having a link for a review needs to be to the author site, with a short code that links to the seller review page.
**Q8.2:** Should custom shortcodes track:
- (a) Total click count only
- (b) Click count with timestamp (analytics)
- (c) Click count with IP/user agent (detailed analytics)
A: c)
---
## 9. Display & Theming
**Q9.1:** For the retail grid display, should the plugin:
- (a) Use its own CSS (bundled with the plugin)
- (b) Inherit styles from the bd-bookshelf theme
- (c) Provide both (own CSS + theme integration)
A: a) use its own CSS to make the appearance optimized
**Q9.2:** For the rating display, should it show:
- (a) Star icons (★☆)
- (b) Numeric rating (4.5/5)
- (c) Both (configurable)
A: c) both
---
## 10. Future Considerations
**Q10.1:** Is there a planned integration with book distributor APIs (Ingram, Baker & Taylor) for automatic metadata population?
A: more interation will be with another plugin - this is the basic plugin to help authors.
**Q10.2:** Should the plugin support multiple authors per book?
A: Yes support up to three authors per book
**Q10.3:** Is there a need for book series management (grouping books into series with ordering)?
A: Book series management is required. The system will allos defining a series - by name. Books will be assigned to a series by the user.

View File

@@ -0,0 +1,197 @@
# BD Bookshelf Plugin - Additional Questions (v0.0.2)
These questions arose from reviewing the answers provided in v0.0.1.
---
## 1. Web Scraping
**Q1.1:** For scraping Amazon and Goodreads, what User-Agent string should we use? Options:
- (a) A realistic browser User-Agent (e.g., Chrome on Windows)
- (b) A custom User-Agent identifying the plugin (e.g., `BD-Bookshelf/1.0`)
- (c) Configurable by admin
A: a)
**Q1.2:** Should we implement a proxy service for scraping to avoid rate limiting? Options:
- (a) No proxy (direct requests only)
- (b) Optional proxy configuration in settings
- (c) Required proxy service integration
A: a) - The plugin should put in delays to avoid rate limiting. The ratings are not real time critical.
**Q1.3:** For scraping Amazon, should we scrape:
- (a) The main product page (`/dp/{ASIN}`)
- (b) The reviews page (`/product-reviews/{ASIN}`)
- (c) Both (product page for rating, reviews page for count)
A: a) The most accessed page.
**Q1.4:** For scraping Goodreads, should we scrape:
- (a) The book page (`/book/show/{id}`)
- (b) The author page (`/author/show/{id}`)
- (c) Both (book page for rating, author page for author info)
A: a)
---
## 2. Series Management
**Q2.1:** Should series have their own pages on the frontend? Options:
- (a) Yes, auto-generate a series page listing all books
- (b) No, series are only for organizational purposes in admin
- (c) Configurable by admin
A: b) a future plugin can expand on this.
**Q2.2:** Should books within a series be automatically ordered by:
- (a) Manual order (user-defined `series_order`)
- (b) Publication date
- (c) Title alphabetically
A: b)
**Q2.3:** Can a book belong to multiple series? Options:
- (a) Yes, a book can be in multiple series
- (b) No, a book can only be in one series
A: b)
---
## 3. Multiple Authors
**Q3.1:** When displaying authors, should the plugin:
- (a) Show all authors equally
- (b) Highlight the primary author
- (c) Configurable by admin
A: a)
**Q3.2:** Should the plugin support author photos/avatars? Options:
- (a) Yes, allow uploading author photos
- (b) No, use WordPress user avatars only
- (c) Use Gravatar based on author email
A: This is the primary author website. The other authors will have a wordpress avatar only.
---
## 4. Affiliate Cookie
**Q4.1:** What should the affiliate cookie duration be? Options:
- (a) 24 hours
- (b) 30 days (standard for most affiliate programs)
- (c) Configurable by admin
A: b) Use the program stanard
**Q4.2:** Should the affiliate cookie store:
- (a) Just the affiliate code
- (b) Affiliate code + source page
- (c) Affiliate code + source page + timestamp
A: a)
---
## 5. SEO Sitemap
**Q5.1:** For the sitemap, what priority should book pages have? Options:
- (a) Fixed priority (e.g., 0.8)
- (b) Configurable per book
- (c) Based on rating (higher rating = higher priority)
A: c)
**Q5.2:** What change frequency should book pages have in sitemap? Options:
- (a) `weekly`
- (b) `monthly`
- (c) Configurable per book
A: b)
---
## 6. Custom Shortcode Analytics
**Q6.1:** How long should click analytics data be retained? Options:
- (a) Indefinitely (no cleanup)
- (b) 90 days
- (c) 1 year
- (d) Configurable by admin
A: c)
**Q6.2:** Should IP addresses be:
- (a) Stored in full
- (b) Anonymized (last octet removed)
- (c) Not stored at all (only count)
A: a) This needs to be tracked to have analytics about using the site for spaming from this site. A hacker could exploit the redirects to make it look like the wordpress site is causing an issue.
---
## 7. GDPR Consent Banner
**Q7.1:** Should the consent banner be dismissible without accepting? Options:
- (a) Yes, user can close without accepting
- (b) No, must accept to continue browsing
- (c) Configurable by admin
A: b)
**Q7.2:** Should the consent banner reappear after:
- (a) 30 days (when cookie expires)
- (b) Every session
- (c) Never (once accepted, stays accepted)
A: a) Stored as part of the cookie and expires with the cookie.
---
## 8. Retail Link Display
**Q8.1:** For the retail grid, what icon style should we use? Options:
- (a) Simple icons (letter/abbreviation)
- (b) Official retail site logos
- (c) Custom icon set bundled with plugin
A: Give the option for the user to use uploaded images/icons. Otherwise use simple icons - option a
**Q8.2:** Should the retail grid be:
- (a) Horizontal row (wraps on mobile)
- (b) Vertical list
- (c) Grid layout (configurable columns)
A: c) use as much of the screen as possible.
---
## 9. Rating Display
**Q9.1:** For star ratings, what icon set should we use? Options:
- (a) Unicode stars (★☆)
- (b) Font Awesome icons
- (c) Custom SVG icons bundled with plugin
A: a) if possible.
**Q9.2:** Should half-star ratings be supported? Options:
- (a) Yes, show half stars (e.g., 4.5)
- (b) No, round to nearest whole star
- (c) Configurable by admin
A: a) half star are needed.
---
## 10. Data Migration
**Q10.1:** If the author has existing books in WordPress (as posts or CPTs), should the plugin provide:
- (a) A migration tool to import into the new system
- (b) No migration (start fresh)
- (c) CSV import functionality
A: a)
---
*Please provide your answers to these questions. Each answer will be incorporated into the final specification.*

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,172 @@
/**
* BD Bookshelf - Admin Styles
*
* @package BD_Bookshelf
*/
/* Form Layout */
.bd-bookshelf-form-columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
}
@media (max-width: 960px) {
.bd-bookshelf-form-columns {
grid-template-columns: 1fr;
}
}
/* Author Rows */
.bd-bookshelf-author-row {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.bd-bookshelf-author-row input {
flex: 1;
}
/* Status Badges */
.bd-bookshelf-status {
display: inline-block;
padding: 3px 8px;
border-radius: 3px;
font-size: 12px;
font-weight: 600;
}
.bd-bookshelf-status.active {
background: #d4edda;
color: #155724;
}
.bd-bookshelf-status.inactive {
background: #f8d7da;
color: #721c24;
}
.bd-bookshelf-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 3px;
font-size: 12px;
font-weight: 600;
background: #e2e3e5;
color: #383d41;
margin-left: 5px;
}
/* Link Status */
.bd-bookshelf-link-status {
display: inline-block;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
margin-left: 5px;
}
.bd-bookshelf-link-status.auto {
background: #cce5ff;
color: #004085;
}
.bd-bookshelf-link-status.custom {
background: #fff3cd;
color: #856404;
}
/* Import Sections */
.bd-bookshelf-import-section {
background: #fff;
border: 1px solid #ccd0d4;
padding: 20px;
margin-bottom: 20px;
}
.bd-bookshelf-import-section h2 {
margin-top: 0;
}
/* Scan Page */
.bd-bookshelf-scan-sections {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 30px;
}
.bd-bookshelf-scan-section {
background: #fff;
border: 1px solid #ccd0d4;
padding: 20px;
border-radius: 4px;
}
@media (max-width: 768px) {
.bd-bookshelf-scan-sections {
grid-template-columns: 1fr;
}
}
.bd-bookshelf-author-card {
background: #f8f9fa;
border: 1px solid #e2e3e5;
padding: 20px;
border-radius: 4px;
margin-bottom: 20px;
}
.bd-bookshelf-author-card h3 {
margin-top: 0;
}
/* Modal */
.bd-bookshelf-modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 100000;
}
.bd-bookshelf-modal-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff;
padding: 30px;
border-radius: 8px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.bd-bookshelf-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.bd-bookshelf-modal-header h2 {
margin: 0;
}
.bd-bookshelf-modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
}
.bd-bookshelf-modal-close:hover {
color: #000;
}

View File

@@ -0,0 +1,846 @@
/**
* BD Bookshelf - Admin JavaScript
*
* @package BD_Bookshelf
*/
(function($) {
'use strict';
$(document).ready(function() {
// Initialize media uploader for book cover
initCoverUploader();
// Initialize media uploader for author photo
initAuthorPhotoUploader();
// Book form
$('#bd-bookshelf-book-form').on('submit', function(e) {
e.preventDefault();
saveBook();
});
// Delete book
$(document).on('click', '.bd-bookshelf-delete', function(e) {
e.preventDefault();
var bookId = $(this).data('id');
if (confirm('Are you sure you want to delete this book?')) {
deleteBook(bookId);
}
});
// Author Modal
initAuthorModal();
// Series Modal
initSeriesModal();
// Shortcode Modal
initShortcodeModal();
// Sites form
$('#bd-bookshelf-sites-form').on('submit', function(e) {
e.preventDefault();
saveSites();
});
// Settings form
$('#bd-bookshelf-settings-form').on('submit', function(e) {
e.preventDefault();
saveSettings();
});
// Import CSV form
$('#bd-bookshelf-import-csv-form').on('submit', function(e) {
e.preventDefault();
importCSV();
});
// Import Posts form
$('#bd-bookshelf-import-posts-form').on('submit', function(e) {
e.preventDefault();
importPosts();
});
// Manual sync
$('#bd-bookshelf-manual-sync').on('click', function() {
var btn = $(this);
btn.prop('disabled', true).text('Syncing...');
$.ajax({
url: bdBookshelfAdmin.ajaxurl,
type: 'POST',
data: {
action: 'bd_bookshelf_sync_ratings',
nonce: bdBookshelfAdmin.nonce
},
success: function(response) {
if (response.success) {
alert(response.data.message);
location.reload();
} else {
alert('Error: ' + response.data.message);
}
},
complete: function() {
btn.prop('disabled', false).text('Sync Now');
}
});
});
// Regenerate retail links
$('#bd-bookshelf-regenerate-links').on('click', function() {
var btn = $(this);
var bookId = btn.data('book-id');
btn.prop('disabled', true).text('Regenerating...');
$.ajax({
url: bdBookshelfAdmin.ajaxurl,
type: 'POST',
data: {
action: 'bd_bookshelf_regenerate_links',
book_id: bookId,
nonce: bdBookshelfAdmin.nonce
},
success: function(response) {
if (response.success) {
$('#bd-bookshelf-retail-links').html(response.data.html);
alert('Retail links regenerated!');
} else {
alert('Error: ' + response.data.message);
}
},
complete: function() {
btn.prop('disabled', false).text('Regenerate Links');
}
});
});
// Scan forms
initScanForms();
});
/**
* Initialize Scan Forms
*/
function initScanForms() {
// Goodreads scan
$('#bd-bookshelf-goodreads-scan-form').on('submit', function(e) {
e.preventDefault();
var authorId = $('#goodreads_author_id').val();
if (!authorId) {
alert('Please enter a Goodreads Author ID');
return;
}
scanAuthor('goodreads', authorId);
});
// Amazon scan
$('#bd-bookshelf-amazon-scan-form').on('submit', function(e) {
e.preventDefault();
var authorId = $('#amazon_author_id').val();
if (!authorId) {
alert('Please enter an Amazon Author ID');
return;
}
scanAuthor('amazon', authorId);
});
// Select all books
$(document).on('change', '#select-all-books', function() {
$('.book-checkbox').prop('checked', $(this).prop('checked'));
});
// Import selected books
$('#bd-bookshelf-import-selected').on('click', function() {
importSelectedBooks();
});
}
/**
* Scan author from source
*/
function scanAuthor(source, authorId) {
var action = source === 'goodreads' ? 'bd_bookshelf_scan_goodreads' : 'bd_bookshelf_scan_amazon';
$('#bd-bookshelf-scan-results').hide();
$('body').css('cursor', 'wait');
$.ajax({
url: bdBookshelfAdmin.ajaxurl,
type: 'POST',
data: {
action: action,
author_id: authorId,
nonce: bdBookshelfAdmin.nonce
},
success: function(response) {
$('body').css('cursor', 'default');
if (response.success) {
displayScanResults(response.data);
} else {
alert('Error: ' + response.data.message);
}
},
error: function() {
$('body').css('cursor', 'default');
alert('An error occurred while scanning. Please try again.');
}
});
}
/**
* Display scan results
*/
function displayScanResults(data) {
var author = data.author;
var books = data.books;
var source = data.source;
// Show results section
$('#bd-bookshelf-scan-results').show();
// Display author info
var authorHtml = '<div class="bd-bookshelf-author-card">';
if (author.photo) {
authorHtml += '<img src="' + author.photo + '" style="width:100px;height:100px;border-radius:50%;margin-right:20px;float:left">';
}
authorHtml += '<h3>' + (author.name || 'Unknown Author') + '</h3>';
if (author.bio) {
authorHtml += '<p>' + author.bio.substring(0, 200) + '...</p>';
}
authorHtml += '<p><strong>Source:</strong> ' + source.charAt(0).toUpperCase() + source.slice(1) + '</p>';
authorHtml += '<p><strong>Books Found:</strong> ' + books.length + '</p>';
authorHtml += '<div style="clear:both"></div></div>';
$('#bd-bookshelf-author-info').html(authorHtml);
// Display books
var booksHtml = '';
books.forEach(function(book, index) {
booksHtml += '<tr>';
booksHtml += '<td><input type="checkbox" class="book-checkbox" value="' + index + '" checked></td>';
booksHtml += '<td>';
if (book.cover_image) {
booksHtml += '<img src="' + book.cover_image + '" style="width:60px;height:auto">';
}
booksHtml += '</td>';
booksHtml += '<td>' + (book.title || '') + '</td>';
booksHtml += '<td>' + (book.isbn || book.isbn13 || book.asin || '') + '</td>';
booksHtml += '<td>' + (book.rating || '0') + '/5</td>';
booksHtml += '<td>' + (book.published || '') + '</td>';
booksHtml += '<td>' + (book.description ? book.description.substring(0, 100) + '...' : '') + '</td>';
booksHtml += '</tr>';
// Store book data in hidden fields
booksHtml += '<input type="hidden" name="book_data[' + index + '][title]" value="' + (book.title || '').replace(/"/g, '&quot;') + '">';
booksHtml += '<input type="hidden" name="book_data[' + index + '][isbn]" value="' + (book.isbn || '') + '">';
booksHtml += '<input type="hidden" name="book_data[' + index + '][isbn13]" value="' + (book.isbn13 || '') + '">';
booksHtml += '<input type="hidden" name="book_data[' + index + '][cover_image]" value="' + (book.cover_image || '') + '">';
booksHtml += '<input type="hidden" name="book_data[' + index + '][rating]" value="' + (book.rating || '0') + '">';
booksHtml += '<input type="hidden" name="book_data[' + index + '][published]" value="' + (book.published || '') + '">';
booksHtml += '<input type="hidden" name="book_data[' + index + '][description]" value="' + (book.description || '').replace(/"/g, '&quot;') + '">';
booksHtml += '<input type="hidden" name="book_data[' + index + '][goodreads_id]" value="' + (book.goodreads_id || '') + '">';
booksHtml += '<input type="hidden" name="book_data[' + index + '][asin]" value="' + (book.asin || '') + '">';
booksHtml += '<input type="hidden" name="book_data[' + index + '][format]" value="' + (book.format || '') + '">';
});
$('#bd-bookshelf-books-list').html(booksHtml);
// Store author data
$('#bd-bookshelf-scan-results').data('author', author);
$('#bd-bookshelf-scan-results').data('source', source);
// Scroll to results
$('html, body').animate({
scrollTop: $('#bd-bookshelf-scan-results').offset().top
}, 500);
}
/**
* Import selected books
*/
function importSelectedBooks() {
var books = [];
var author = $('#bd-bookshelf-scan-results').data('author');
var source = $('#bd-bookshelf-scan-results').data('source');
$('.book-checkbox:checked').each(function() {
var index = $(this).val();
var bookData = {
title: $('input[name="book_data[' + index + '][title]"]').val(),
isbn: $('input[name="book_data[' + index + '][isbn]"]').val(),
isbn13: $('input[name="book_data[' + index + '][isbn13]"]').val(),
cover_image: $('input[name="book_data[' + index + '][cover_image]"]').val(),
rating: $('input[name="book_data[' + index + '][rating]"]').val(),
published: $('input[name="book_data[' + index + '][published]"]').val(),
description: $('input[name="book_data[' + index + '][description]"]').val(),
goodreads_id: $('input[name="book_data[' + index + '][goodreads_id]"]').val(),
asin: $('input[name="book_data[' + index + '][asin]"]').val() || '',
format: $('input[name="book_data[' + index + '][format]"]').val() || '',
};
books.push(bookData);
});
if (books.length === 0) {
alert('Please select at least one book to import');
return;
}
if (!confirm('Import ' + books.length + ' book(s)?')) {
return;
}
$('body').css('cursor', 'wait');
// Build form data manually for proper nested array handling
var formData = new FormData();
formData.append('action', 'bd_bookshelf_import_scraped');
formData.append('nonce', bdBookshelfAdmin.nonce);
formData.append('source', source);
// Add author data
if (author) {
formData.append('author[name]', author.name || '');
formData.append('author[bio]', author.bio || '');
formData.append('author[photo]', author.photo || '');
formData.append('author[id]', author.id || '');
}
// Add books array
books.forEach(function(book, i) {
formData.append('books[' + i + '][title]', book.title || '');
formData.append('books[' + i + '][isbn]', book.isbn || '');
formData.append('books[' + i + '][isbn13]', book.isbn13 || '');
formData.append('books[' + i + '][cover_image]', book.cover_image || '');
formData.append('books[' + i + '][rating]', book.rating || '0');
formData.append('books[' + i + '][published]', book.published || '');
formData.append('books[' + i + '][description]', book.description || '');
formData.append('books[' + i + '][goodreads_id]', book.goodreads_id || '');
formData.append('books[' + i + '][asin]', book.asin || '');
formData.append('books[' + i + '][format]', book.format || '');
});
$.ajax({
url: bdBookshelfAdmin.ajaxurl,
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
$('body').css('cursor', 'default');
if (response.success) {
alert(response.data.message);
if (response.data.imported > 0 || response.data.updated > 0) {
location.reload();
}
} else {
alert('Error: ' + response.data.message);
}
},
error: function() {
$('body').css('cursor', 'default');
alert('An error occurred while importing. Please try again.');
}
});
}
/**
* Initialize Cover Image Uploader
*/
function initCoverUploader() {
var coverFrame;
$('#cover-upload-btn').on('click', function(e) {
e.preventDefault();
if (coverFrame) {
coverFrame.open();
return;
}
coverFrame = wp.media({
title: bdBookshelfAdmin.mediaTitle,
button: { text: bdBookshelfAdmin.mediaButton },
multiple: false,
library: { type: 'image' }
});
coverFrame.on('select', function() {
var attachment = coverFrame.state().get('selection').first().toJSON();
var url = attachment.sizes && attachment.sizes.medium ? attachment.sizes.medium.url : attachment.url;
$('#cover_image').val(attachment.url);
$('#cover-preview').html('<img src="' + url + '" style="max-width:200px;height:auto">');
$('#cover-remove-btn').show();
});
coverFrame.open();
});
$('#cover-remove-btn').on('click', function(e) {
e.preventDefault();
$('#cover_image').val('');
$('#cover-preview').html('');
$(this).hide();
});
}
/**
* Initialize Author Photo Uploader
*/
function initAuthorPhotoUploader() {
var photoFrame;
$('#author-photo-btn').on('click', function(e) {
e.preventDefault();
if (photoFrame) {
photoFrame.open();
return;
}
photoFrame = wp.media({
title: 'Select Author Photo',
button: { text: 'Use This Photo' },
multiple: false,
library: { type: 'image' }
});
photoFrame.on('select', function() {
var attachment = photoFrame.state().get('selection').first().toJSON();
var url = attachment.sizes && attachment.sizes.thumbnail ? attachment.sizes.thumbnail.url : attachment.url;
$('#author_photo_url').val(attachment.url);
$('#author-photo-preview').html('<img src="' + url + '" style="max-width:100px;height:auto">');
$('#author-photo-remove').show();
});
photoFrame.open();
});
$('#author-photo-remove').on('click', function(e) {
e.preventDefault();
$('#author_photo_url').val('');
$('#author-photo-preview').html('');
$(this).hide();
});
}
/**
* Initialize Author Modal
*/
function initAuthorModal() {
// Open modal for new author
$('#bd-bookshelf-add-author').on('click', function() {
$('#author_id').val(0);
$('#author_display_name').val('');
$('#author_email').val('');
$('#author_website').val('');
$('#author_bio').val('');
$('#author_goodreads_id').val('');
$('#author_photo_url').val('');
$('#author-photo-preview').html('');
$('#author-photo-remove').hide();
$('#bd-bookshelf-author-modal-title').text('Add New Author');
$('#bd-bookshelf-author-modal').show();
});
// Edit author
$(document).on('click', '.bd-bookshelf-edit-author', function(e) {
e.preventDefault();
var authorId = $(this).data('id');
// Fetch author data
$.ajax({
url: bdBookshelfAdmin.ajaxurl,
type: 'POST',
data: {
action: 'bd_bookshelf_get_author',
id: authorId,
nonce: bdBookshelfAdmin.nonce
},
success: function(response) {
if (response.success) {
var author = response.data;
$('#author_id').val(author.id);
$('#author_display_name').val(author.display_name);
$('#author_email').val(author.email);
$('#author_website').val(author.website);
$('#author_bio').val(author.bio);
$('#author_goodreads_id').val(author.goodreads_author_id);
$('#author_photo_url').val(author.photo_url);
if (author.photo_url) {
$('#author-photo-preview').html('<img src="' + author.photo_url + '" style="max-width:100px;height:auto">');
$('#author-photo-remove').show();
} else {
$('#author-photo-preview').html('');
$('#author-photo-remove').hide();
}
$('#bd-bookshelf-author-modal-title').text('Edit Author');
$('#bd-bookshelf-author-modal').show();
}
}
});
});
// Delete author
$(document).on('click', '.bd-bookshelf-delete-author', function(e) {
e.preventDefault();
var authorId = $(this).data('id');
if (confirm('Are you sure you want to delete this author?')) {
$.ajax({
url: bdBookshelfAdmin.ajaxurl,
type: 'POST',
data: {
action: 'bd_bookshelf_delete_author',
id: authorId,
nonce: bdBookshelfAdmin.nonce
},
success: function(response) {
if (response.success) {
location.reload();
} else {
alert('Error: ' + response.data.message);
}
}
});
}
});
// Save author
$('#bd-bookshelf-author-form').on('submit', function(e) {
e.preventDefault();
var data = $(this).serialize();
$.ajax({
url: bdBookshelfAdmin.ajaxurl,
type: 'POST',
data: data + '&action=bd_bookshelf_save_author',
success: function(response) {
if (response.success) {
location.reload();
} else {
alert('Error: ' + response.data.message);
}
}
});
});
// Close modal
$(document).on('click', '.bd-bookshelf-modal-close', function() {
$(this).closest('.bd-bookshelf-modal').hide();
});
}
/**
* Initialize Series Modal
*/
function initSeriesModal() {
// Open modal for new series
$('#bd-bookshelf-add-series').on('click', function() {
$('#series_id_input').val(0);
$('#series_name').val('');
$('#series_description').val('');
$('#bd-bookshelf-series-modal-title').text('Add New Series');
$('#bd-bookshelf-series-modal').show();
});
// Edit series
$(document).on('click', '.bd-bookshelf-edit-series', function(e) {
e.preventDefault();
var seriesId = $(this).data('id');
// Fetch series data
$.ajax({
url: bdBookshelfAdmin.ajaxurl,
type: 'POST',
data: {
action: 'bd_bookshelf_get_series',
id: seriesId,
nonce: bdBookshelfAdmin.nonce
},
success: function(response) {
if (response.success) {
var series = response.data;
$('#series_id_input').val(series.id);
$('#series_name').val(series.name);
$('#series_description').val(series.description);
$('#bd-bookshelf-series-modal-title').text('Edit Series');
$('#bd-bookshelf-series-modal').show();
}
}
});
});
// Delete series
$(document).on('click', '.bd-bookshelf-delete-series', function(e) {
e.preventDefault();
var seriesId = $(this).data('id');
if (confirm('Are you sure you want to delete this series? Books in this series will be unlinked.')) {
$.ajax({
url: bdBookshelfAdmin.ajaxurl,
type: 'POST',
data: {
action: 'bd_bookshelf_delete_series',
id: seriesId,
nonce: bdBookshelfAdmin.nonce
},
success: function(response) {
if (response.success) {
location.reload();
} else {
alert('Error: ' + response.data.message);
}
}
});
}
});
// Save series
$('#bd-bookshelf-series-form').on('submit', function(e) {
e.preventDefault();
var data = $(this).serialize();
$.ajax({
url: bdBookshelfAdmin.ajaxurl,
type: 'POST',
data: data + '&action=bd_bookshelf_save_series',
success: function(response) {
if (response.success) {
location.reload();
} else {
alert('Error: ' + response.data.message);
}
}
});
});
}
/**
* Initialize Shortcode Modal
*/
function initShortcodeModal() {
// Open modal for new shortcode
$('#bd-bookshelf-add-shortcode').on('click', function() {
$('#shortcode_id').val(0);
$('#shortcode_slug').val('');
$('#shortcode_target').val('');
$('#shortcode_redirect').val('301');
$('#shortcode_description').val('');
$('#shortcode_slug').prop('readonly', false);
$('#bd-bookshelf-shortcode-modal-title').text('Add New Shortcode');
$('#bd-bookshelf-shortcode-modal').show();
});
// Edit shortcode
$(document).on('click', '.bd-bookshelf-edit-shortcode', function(e) {
e.preventDefault();
var shortcodeId = $(this).data('id');
$.ajax({
url: bdBookshelfAdmin.ajaxurl,
type: 'POST',
data: {
action: 'bd_bookshelf_get_shortcode',
id: shortcodeId,
nonce: bdBookshelfAdmin.nonce
},
success: function(response) {
if (response.success) {
var sc = response.data;
$('#shortcode_id').val(sc.id);
$('#shortcode_slug').val(sc.slug).prop('readonly', true);
$('#shortcode_target').val(sc.target_url);
$('#shortcode_redirect').val(sc.redirect_type);
$('#shortcode_description').val(sc.description);
$('#bd-bookshelf-shortcode-modal-title').text('Edit Shortcode');
$('#bd-bookshelf-shortcode-modal').show();
}
}
});
});
// Delete shortcode
$(document).on('click', '.bd-bookshelf-delete-shortcode', function(e) {
e.preventDefault();
var shortcodeId = $(this).data('id');
if (confirm('Are you sure you want to delete this shortcode?')) {
$.ajax({
url: bdBookshelfAdmin.ajaxurl,
type: 'POST',
data: {
action: 'bd_bookshelf_delete_shortcode',
id: shortcodeId,
nonce: bdBookshelfAdmin.nonce
},
success: function(response) {
if (response.success) {
location.reload();
} else {
alert('Error: ' + response.data.message);
}
}
});
}
});
// Save shortcode
$('#bd-bookshelf-shortcode-form').on('submit', function(e) {
e.preventDefault();
var data = $(this).serialize();
$.ajax({
url: bdBookshelfAdmin.ajaxurl,
type: 'POST',
data: data + '&action=bd_bookshelf_save_shortcode',
success: function(response) {
if (response.success) {
location.reload();
} else {
alert('Error: ' + response.data.message);
}
}
});
});
}
/**
* Save Book
*/
function saveBook() {
var form = $('#bd-bookshelf-book-form');
var data = form.serialize();
$.ajax({
url: bdBookshelfAdmin.ajaxurl,
type: 'POST',
data: data + '&action=bd_bookshelf_save_book',
success: function(response) {
if (response.success) {
alert('Book saved!');
if (!form.find('input[name="book_id"]').val() || form.find('input[name="book_id"]').val() === '0') {
window.location.href = window.location.href.split('&book_id=')[0] + '&book_id=' + response.data.id;
}
} else {
alert('Error: ' + response.data.message);
}
}
});
}
/**
* Delete Book
*/
function deleteBook(bookId) {
$.ajax({
url: bdBookshelfAdmin.ajaxurl,
type: 'POST',
data: {
action: 'bd_bookshelf_delete_book',
id: bookId,
nonce: bdBookshelfAdmin.nonce
},
success: function(response) {
if (response.success) {
location.reload();
} else {
alert('Error: ' + response.data.message);
}
}
});
}
/**
* Save Sites/Affiliate Codes
*/
function saveSites() {
var form = $('#bd-bookshelf-sites-form');
var data = form.serialize();
$.ajax({
url: bdBookshelfAdmin.ajaxurl,
type: 'POST',
data: data + '&action=bd_bookshelf_save_sites',
success: function(response) {
if (response.success) {
alert('Affiliate codes saved!');
} else {
alert('Error: ' + response.data.message);
}
}
});
}
/**
* Save Settings
*/
function saveSettings() {
var form = $('#bd-bookshelf-settings-form');
var data = form.serialize();
$.ajax({
url: bdBookshelfAdmin.ajaxurl,
type: 'POST',
data: data + '&action=bd_bookshelf_save_settings',
success: function(response) {
if (response.success) {
alert('Settings saved!');
} else {
alert('Error: ' + response.data.message);
}
}
});
}
/**
* Import CSV
*/
function importCSV() {
var form = $('#bd-bookshelf-import-csv-form');
var formData = new FormData(form[0]);
formData.append('action', 'bd_bookshelf_import_csv');
$.ajax({
url: bdBookshelfAdmin.ajaxurl,
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
if (response.success) {
$('#bd-bookshelf-import-results').show();
$('#bd-bookshelf-import-message').text(response.data.message);
} else {
alert('Error: ' + response.data.message);
}
}
});
}
/**
* Import Posts
*/
function importPosts() {
var form = $('#bd-bookshelf-import-posts-form');
var data = form.serialize();
$.ajax({
url: bdBookshelfAdmin.ajaxurl,
type: 'POST',
data: data + '&action=bd_bookshelf_import_posts',
success: function(response) {
if (response.success) {
alert(response.data.message);
} else {
alert('Error: ' + response.data.message);
}
}
});
}
})(jQuery);

View File

@@ -0,0 +1,155 @@
<?php
/**
* 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();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,293 @@
<?php
/**
* Amazon Scraper class
*
* Fetches author and book data from Amazon
*
* @package BD_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
class BDM_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';
/**
* Search Amazon for a book by title and author
*
* @param string $title Book title.
* @param string $author Author name.
* @return array List of books found.
*/
public function search_books($title, $author = '') {
$query = $title;
if ($author) {
$query .= ' ' . $author;
}
$url = 'https://www.amazon.com/s?k=' . urlencode($query) . '&i=stripbooks';
$response = wp_remote_get($url, [
'timeout' => 30,
'headers' => [
'User-Agent' => $this->user_agent,
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language' => 'en-US,en;q=0.5',
],
]);
if (is_wp_error($response)) {
return ['error' => $response->get_error_message()];
}
$body = wp_remote_retrieve_body($response);
return $this->parse_search_results($body);
}
/**
* Parse Amazon search results
*/
private function parse_search_results($html) {
$books = [];
// Match search result items with data-asin
if (preg_match_all('/<div[^>]*data-asin="([A-Z0-9]{10})"[^>]*class="[^"]*s-result-item[^"]*"[^>]*>(.*?)<\/div>\s*<\/div>\s*<\/div>/si', $html, $matches)) {
foreach ($matches as $index => $match) {
$asin = $match[1];
$item_html = $match[2];
$book = $this->parse_search_item($item_html, $asin);
if ($book) {
$books[] = $book;
}
}
}
// Alternative pattern
if (empty($books)) {
if (preg_match_all('/data-asin="([A-Z0-9]{10})"/si', $html, $asin_matches)) {
foreach ($asin_matches[1] as $asin) {
// Try to find context around this ASIN
$pattern = '/data-asin="' . preg_quote($asin) . '"[^>]*>(.*?)(?=data-asin="|$)/si';
if (preg_match($pattern, $html, $context_match)) {
$book = $this->parse_search_item($context_match[1], $asin);
if ($book) {
$books[] = $book;
}
}
}
}
}
return $books;
}
/**
* Parse a single search result item
*/
private function parse_search_item($html, $asin) {
$book = [
'asin' => $asin,
'title' => '',
'author' => '',
'format' => '',
'rating' => 0,
'ratings_count' => 0,
'cover_image' => '',
'url' => 'https://www.amazon.com/dp/' . $asin,
];
// Extract title
if (preg_match('/<h2[^>]*>.*?<span[^>]*>(.*?)<\/span>/si', $html, $matches)) {
$book['title'] = trim(strip_tags($matches[1]));
} elseif (preg_match('/<a[^>]*>.*?<span[^>]*class="[^"]*a-size[^"]*"[^>]*>(.*?)<\/span>/si', $html, $matches)) {
$book['title'] = trim(strip_tags($matches[1]));
}
// Extract author
if (preg_match('/<span[^>]*class="[^"]*a-size[^"]*"[^>]*>\s*by\s+<a[^>]*>(.*?)<\/a>/si', $html, $matches)) {
$book['author'] = trim(strip_tags($matches[1]));
}
// Extract format (Kindle, Paperback, Hardcover, Audio)
if (preg_match('/(Kindle\s*Edition|Paperback|Hardcover|Audio\s*CD|Audible|MP3\s*CD)/si', $html, $matches)) {
$book['format'] = trim($matches[1]);
}
// Extract rating
if (preg_match('/(\d+\.?\d*)\s*out\s*of\s*5\s*stars?/si', $html, $matches)) {
$book['rating'] = (float) $matches[1];
}
// Extract ratings count
if (preg_match('/([\d,]+)\s*ratings?/si', $html, $matches)) {
$book['ratings_count'] = absint(str_replace(',', '', $matches[1]));
}
// Extract cover image
if (preg_match('/<img[^>]*src="(https:\/\/[^"]*\.jpg)"/si', $html, $matches)) {
$book['cover_image'] = $matches[1];
}
if (empty($book['title'])) {
return null;
}
return $book;
}
/**
* Look up book by ASIN and get full details including ISBN
*
* @param string $asin Amazon ASIN.
* @return array Book details.
*/
public function lookup_by_asin($asin) {
$url = 'https://www.amazon.com/dp/' . $asin;
$response = wp_remote_get($url, [
'timeout' => 30,
'headers' => [
'User-Agent' => $this->user_agent,
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language' => 'en-US,en;q=0.5',
],
]);
if (is_wp_error($response)) {
return ['error' => $response->get_error_message()];
}
$body = wp_remote_retrieve_body($response);
return $this->parse_product_page($body, $asin);
}
/**
* Parse Amazon product page for full book details
*/
private function parse_product_page($html, $asin) {
$book = [
'asin' => $asin,
'title' => '',
'author' => '',
'isbn' => '',
'isbn13' => '',
'rating' => 0,
'ratings_count' => 0,
'cover_image' => '',
'description' => '',
'formats' => [],
];
// Extract title
if (preg_match('/<h1[^>]*id="title"[^>]*>(.*?)<\/h1>/si', $html, $matches)) {
$book['title'] = trim(strip_tags($matches[1]));
}
// Extract author
if (preg_match('/by\s+<a[^>]*>(.*?)<\/a>/si', $html, $matches)) {
$book['author'] = trim(strip_tags($matches[1]));
}
// Extract rating
if (preg_match('/(\d+\.?\d*)\s*out\s*of\s*5\s*stars?/si', $html, $matches)) {
$book['rating'] = (float) $matches[1];
}
// Extract ratings count
if (preg_match('/([\d,]+)\s*ratings?/si', $html, $matches)) {
$book['ratings_count'] = absint(str_replace(',', '', $matches[1]));
}
// Extract cover image
if (preg_match('/<img[^>]*id="landingImage"[^>]*src="([^"]+)"/si', $html, $matches)) {
$book['cover_image'] = $matches[1];
}
// Extract ISBN from product details
if (preg_match('/ISBN-10:\s*(\d{10})/si', $html, $matches)) {
$book['isbn'] = $matches[1];
}
if (preg_match('/ISBN-13:\s*(\d{13})/si', $html, $matches)) {
$book['isbn13'] = $matches[1];
}
// Extract ASIN
if (preg_match('/ASIN:\s*([A-Z0-9]{10})/si', $html, $matches)) {
$book['asin'] = $matches[1];
}
// Extract description
if (preg_match('/<div[^>]*id="bookDescription_feature_div"[^>]*>.*?<span[^>]*>(.*?)<\/span>/si', $html, $matches)) {
$book['description'] = trim(strip_tags($matches[1]));
}
// Look for other format links (Kindle, Paperback, Hardcover)
if (preg_match_all('/<li[^>]*>.*?<a[^>]*href="\/dp\/([A-Z0-9]{10})"[^>]*>.*?<span[^>]*>(.*?)<\/span>/si', $html, $matches)) {
foreach ($matches[1] as $index => $format_asin) {
$format_name = trim(strip_tags($matches[2][$index]));
if ($format_asin !== $asin && !empty($format_name)) {
$book['formats'][] = [
'asin' => $format_asin,
'format' => $format_name,
'url' => 'https://www.amazon.com/dp/' . $format_asin,
];
}
}
}
return $book;
}
/**
* Get all format ASINs for a book by searching
*
* @param string $title Book title.
* @param string $author Author name.
* @return array Format => ASIN mapping.
*/
public function get_format_asins($title, $author = '') {
$formats = [
'kindle' => '',
'paperback' => '',
'hardcover' => '',
'audio' => '',
];
// Search for the book
$results = $this->search_books($title, $author);
if (isset($results['error']) || empty($results)) {
return $formats;
}
// Match results to formats
foreach ($results as $book) {
$book_title = strtolower($book['title'] ?? '');
$book_format = strtolower($book['format'] ?? '');
// Check if this is the same book (title match)
similar_text(strtolower($title), $book_title, $percent);
if ($percent < 60) {
continue;
}
if (strpos($book_format, 'kindle') !== false || strpos($book_format, 'ebook') !== false) {
$formats['kindle'] = $book['asin'];
} elseif (strpos($book_format, 'paperback') !== false) {
$formats['paperback'] = $book['asin'];
} elseif (strpos($book_format, 'hardcover') !== false) {
$formats['hardcover'] = $book['asin'];
} elseif (strpos($book_format, 'audio') !== false || strpos($book_format, 'audible') !== false) {
$formats['audio'] = $book['asin'];
}
}
return $formats;
}
}

View File

@@ -0,0 +1,176 @@
<?php
/**
* Authors Management class
*
* @package BD_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
class BDM_Authors {
/**
* Get all authors
*/
public static function get_authors($args = []) {
global $wpdb;
$table = BDM_Database::table('authors');
$defaults = [
'number' => 20,
'offset' => 0,
'orderby' => 'display_name',
'order' => 'ASC',
'search' => '',
];
$args = wp_parse_args($args, $defaults);
$where = '1=1';
$params = [];
if (!empty($args['search'])) {
$where .= ' AND display_name LIKE %s';
$params[] = '%' . $wpdb->esc_like($args['search']) . '%';
}
$order = strtoupper($args['order']) === 'DESC' ? 'DESC' : 'ASC';
$query = "SELECT * FROM {$table} WHERE {$where} ORDER BY {$args['orderby']} {$order} LIMIT %d OFFSET %d";
$params[] = $args['number'];
$params[] = $args['offset'];
if (!empty($params)) {
$query = $wpdb->prepare($query, ...$params);
}
return $wpdb->get_results($query);
}
/**
* Get author by ID
*/
public static function get_author($author_id) {
global $wpdb;
$table = BDM_Database::table('authors');
return $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$table} WHERE id = %d",
$author_id
)
);
}
/**
* Create author
*/
public static function create_author($data) {
global $wpdb;
$table = BDM_Database::table('authors');
$result = $wpdb->insert($table, [
'display_name' => sanitize_text_field($data['display_name']),
'email' => sanitize_email($data['email'] ?? ''),
'website' => esc_url_raw($data['website'] ?? ''),
'bio' => wp_kses_post($data['bio'] ?? ''),
'goodreads_author_id' => sanitize_text_field($data['goodreads_author_id'] ?? ''),
'photo_url' => esc_url_raw($data['photo_url'] ?? ''),
]);
return $result ? $wpdb->insert_id : false;
}
/**
* Update author
*/
public static function update_author($author_id, $data) {
global $wpdb;
$table = BDM_Database::table('authors');
$update_data = [];
if (isset($data['display_name'])) {
$update_data['display_name'] = sanitize_text_field($data['display_name']);
}
if (isset($data['email'])) {
$update_data['email'] = sanitize_email($data['email']);
}
if (isset($data['website'])) {
$update_data['website'] = esc_url_raw($data['website']);
}
if (isset($data['bio'])) {
$update_data['bio'] = wp_kses_post($data['bio']);
}
if (isset($data['goodreads_author_id'])) {
$update_data['goodreads_author_id'] = sanitize_text_field($data['goodreads_author_id']);
}
if (isset($data['photo_url'])) {
$update_data['photo_url'] = esc_url_raw($data['photo_url']);
}
if (empty($update_data)) {
return false;
}
$result = $wpdb->update(
$table,
$update_data,
['id' => $author_id],
null,
['%d']
);
return $result !== false;
}
/**
* Delete author
*/
public static function delete_author($author_id) {
global $wpdb;
// Remove from book_authors
$wpdb->delete(BDM_Database::table('book_authors'), ['author_id' => $author_id], ['%d']);
// Delete author
$table = BDM_Database::table('authors');
return $wpdb->delete($table, ['id' => $author_id], ['%d']) !== false;
}
/**
* Count authors
*/
public static function count_authors($search = '') {
global $wpdb;
$table = BDM_Database::table('authors');
$where = '1=1';
$params = [];
if (!empty($search)) {
$where .= ' AND display_name LIKE %s';
$params[] = '%' . $wpdb->esc_like($search) . '%';
}
$query = "SELECT COUNT(*) FROM {$table} WHERE {$where}";
if (!empty($params)) {
$query = $wpdb->prepare($query, ...$params);
}
return $wpdb->get_var($query);
}
}

View File

@@ -0,0 +1,477 @@
<?php
/**
* Book Manager class
*
* @package BD_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
class BDM_Book_Manager {
/**
* Get all books
*/
public static function get_books($args = []) {
global $wpdb;
$table = BDM_Database::table('books');
$defaults = [
'number' => 20,
'offset' => 0,
'orderby' => 'title',
'order' => 'ASC',
'search' => '',
];
$args = wp_parse_args($args, $defaults);
$where = '1=1';
$params = [];
if (!empty($args['search'])) {
$where .= ' AND title LIKE %s';
$params[] = '%' . $wpdb->esc_like($args['search']) . '%';
}
$order = strtoupper($args['order']) === 'DESC' ? 'DESC' : 'ASC';
$allowed_orders = ['title', 'created_at', 'updated_at', 'rating_amazon', 'rating_goodreads'];
$orderby = in_array($args['orderby'], $allowed_orders) ? $args['orderby'] : 'title';
$query = "SELECT * FROM {$table} WHERE {$where} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d";
$params[] = $args['number'];
$params[] = $args['offset'];
if (!empty($params)) {
$query = $wpdb->prepare($query, ...$params);
}
$books = $wpdb->get_results($query);
return $books;
}
/**
* Get book by ID
*/
public static function get_book($book_id) {
global $wpdb;
$table = BDM_Database::table('books');
$book = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$table} WHERE id = %d",
$book_id
)
);
if ($book) {
$book->authors = self::get_book_authors($book_id);
$book->retail_links = self::get_book_retail_links($book_id);
$book->seo = BDM_SEO::get_book_seo($book_id);
}
return $book;
}
/**
* Get book by title
*/
public static function get_book_by_title($title) {
global $wpdb;
$table = BDM_Database::table('books');
$book = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$table} WHERE title = %s",
$title
)
);
return $book;
}
/**
* Get book by slug
*/
public static function get_book_by_slug($slug) {
global $wpdb;
$table = BDM_Database::table('books');
$book = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$table} WHERE slug = %s",
$slug
)
);
if ($book) {
$book->authors = self::get_book_authors($book->id);
$book->retail_links = self::get_book_retail_links($book->id);
$book->seo = BDM_SEO::get_book_seo($book->id);
}
return $book;
}
/**
* Create new book
*/
public static function create_book($data) {
global $wpdb;
$table = BDM_Database::table('books');
$slug = sanitize_title($data['slug'] ?? $data['title'] ?? '');
$insert_data = [
'title' => sanitize_text_field($data['title']),
'slug' => $slug,
'description' => wp_kses_post($data['description'] ?? ''),
'cover_image' => esc_url_raw($data['cover_image'] ?? ''),
'isbn_ebook' => self::sanitize_isbn($data['isbn_ebook'] ?? ''),
'isbn_paperback' => self::sanitize_isbn($data['isbn_paperback'] ?? ''),
'isbn_hardcover' => self::sanitize_isbn($data['isbn_hardcover'] ?? ''),
'isbn_audio' => self::sanitize_isbn($data['isbn_audio'] ?? ''),
'asin_ebook' => sanitize_text_field($data['asin_ebook'] ?? ''),
'asin_paperback' => sanitize_text_field($data['asin_paperback'] ?? ''),
'asin_hardcover' => sanitize_text_field($data['asin_hardcover'] ?? ''),
'asin_audio' => sanitize_text_field($data['asin_audio'] ?? ''),
'amazon_asin' => sanitize_text_field($data['amazon_asin'] ?? ''),
'goodreads_id' => sanitize_text_field($data['goodreads_id'] ?? ''),
'series_id' => absint($data['series_id'] ?? 0),
];
// Handle ratings
if (isset($data['rating_amazon'])) {
$insert_data['rating_amazon'] = (float) $data['rating_amazon'];
}
if (isset($data['rating_goodreads'])) {
$insert_data['rating_goodreads'] = (float) $data['rating_goodreads'];
}
$result = $wpdb->insert($table, $insert_data);
if ($result) {
$book_id = $wpdb->insert_id;
// Handle authors - new system uses author_ids
if (!empty($data['author_ids']) && is_array($data['author_ids'])) {
self::update_book_authors_by_id($book_id, $data['author_ids']);
} elseif (!empty($data['authors']) && is_array($data['authors'])) {
// Legacy support
self::update_book_authors($book_id, $data['authors']);
}
return $book_id;
}
return false;
}
/**
* Update book
*/
public static function update_book($book_id, $data) {
global $wpdb;
$table = BDM_Database::table('books');
$update_data = [];
if (isset($data['title'])) {
$update_data['title'] = sanitize_text_field($data['title']);
}
if (isset($data['description'])) {
$update_data['description'] = wp_kses_post($data['description']);
}
if (isset($data['cover_image'])) {
$update_data['cover_image'] = esc_url_raw($data['cover_image']);
}
if (isset($data['isbn_ebook'])) {
$update_data['isbn_ebook'] = self::sanitize_isbn($data['isbn_ebook']);
}
if (isset($data['isbn_paperback'])) {
$update_data['isbn_paperback'] = self::sanitize_isbn($data['isbn_paperback']);
}
if (isset($data['isbn_hardcover'])) {
$update_data['isbn_hardcover'] = self::sanitize_isbn($data['isbn_hardcover']);
}
if (isset($data['isbn_audio'])) {
$update_data['isbn_audio'] = self::sanitize_isbn($data['isbn_audio']);
}
if (isset($data['asin_ebook'])) {
$update_data['asin_ebook'] = sanitize_text_field($data['asin_ebook']);
}
if (isset($data['asin_paperback'])) {
$update_data['asin_paperback'] = sanitize_text_field($data['asin_paperback']);
}
if (isset($data['asin_hardcover'])) {
$update_data['asin_hardcover'] = sanitize_text_field($data['asin_hardcover']);
}
if (isset($data['asin_audio'])) {
$update_data['asin_audio'] = sanitize_text_field($data['asin_audio']);
}
if (isset($data['amazon_asin'])) {
$update_data['amazon_asin'] = sanitize_text_field($data['amazon_asin']);
}
if (isset($data['goodreads_id'])) {
$update_data['goodreads_id'] = sanitize_text_field($data['goodreads_id']);
}
if (isset($data['series_id'])) {
$update_data['series_id'] = absint($data['series_id']);
}
if (empty($update_data)) {
return false;
}
$result = $wpdb->update(
$table,
$update_data,
['id' => $book_id],
null,
['%d']
);
// Handle authors - new system uses author_ids
if (isset($data['author_ids']) && is_array($data['author_ids'])) {
self::update_book_authors_by_id($book_id, $data['author_ids']);
} elseif (isset($data['authors']) && is_array($data['authors'])) {
// Legacy support
self::update_book_authors($book_id, $data['authors']);
}
return $result !== false;
}
/**
* Delete book
*/
public static function delete_book($book_id) {
global $wpdb;
// Delete related data
$wpdb->delete(BDM_Database::table('book_authors'), ['book_id' => $book_id], ['%d']);
$wpdb->delete(BDM_Database::table('retail_links'), ['book_id' => $book_id], ['%d']);
$wpdb->delete(BDM_Database::table('book_seo'), ['book_id' => $book_id], ['%d']);
// Delete book
$table = BDM_Database::table('books');
$result = $wpdb->delete($table, ['id' => $book_id], ['%d']);
return $result !== false;
}
/**
* Get book authors
*/
public static function get_book_authors($book_id) {
global $wpdb;
$table = BDM_Database::table('book_authors');
$authors = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$table} WHERE book_id = %d ORDER BY sort_order ASC",
$book_id
)
);
return $authors;
}
/**
* Update book authors by author IDs
*/
public static function update_book_authors_by_id($book_id, $author_ids) {
global $wpdb;
$table = BDM_Database::table('book_authors');
$authors_table = BDM_Database::table('authors');
// Delete existing authors
$wpdb->delete($table, ['book_id' => $book_id], ['%d']);
// Filter out zero values
$author_ids = array_filter(array_map('absint', $author_ids));
// Insert new authors
foreach ($author_ids as $index => $author_id) {
if ($author_id <= 0) continue;
// Get author details
$author = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$authors_table} WHERE id = %d",
$author_id
)
);
if ($author) {
$wpdb->insert($table, [
'book_id' => $book_id,
'author_name' => $author->display_name,
'author_id' => $author_id,
'goodreads_author_id' => $author->goodreads_author_id,
'is_primary' => ($index === 0) ? 1 : 0,
'sort_order' => $index + 1,
]);
}
}
}
/**
* Update book authors (legacy)
*/
public static function update_book_authors($book_id, $authors) {
global $wpdb;
$table = BDM_Database::table('book_authors');
// Delete existing authors
$wpdb->delete($table, ['book_id' => $book_id], ['%d']);
// Insert new authors
foreach ($authors as $index => $author) {
$wpdb->insert($table, [
'book_id' => $book_id,
'author_name' => sanitize_text_field($author['name']),
'author_id' => absint($author['user_id'] ?? 0),
'goodreads_author_id' => sanitize_text_field($author['goodreads_id'] ?? ''),
'is_primary' => ($index === 0) ? 1 : 0,
'sort_order' => $index + 1,
]);
}
}
/**
* Get book retail links
*/
public static function get_book_retail_links($book_id, $format = '') {
global $wpdb;
$table = BDM_Database::table('retail_links');
$sites_table = BDM_Database::table('retail_sites');
$where = 'rl.book_id = %d AND rs.is_active = 1';
$params = [$book_id];
if (!empty($format)) {
$where .= ' AND rl.format = %s';
$params[] = $format;
}
$query = $wpdb->prepare(
"SELECT rl.*, rs.name as site_name, rs.icon as site_icon, rs.affiliate_param, rs.affiliate_code
FROM {$table} rl
LEFT JOIN {$sites_table} rs ON rl.site_id = rs.id
WHERE {$where}
ORDER BY rs.name ASC",
...$params
);
$links = $wpdb->get_results($query);
return $links;
}
/**
* Get books by series
*/
public static function get_books_by_series($series_id) {
global $wpdb;
$table = BDM_Database::table('books');
$books = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$table} WHERE series_id = %d ORDER BY created_at ASC",
$series_id
)
);
return $books;
}
/**
* Sanitize ISBN
*/
private static function sanitize_isbn($isbn) {
return preg_replace('/[^0-9X]/i', '', $isbn);
}
/**
* Get series list
*/
public static function get_series_list() {
global $wpdb;
$table = BDM_Database::table('series');
return $wpdb->get_results("SELECT * FROM {$table} ORDER BY name ASC");
}
/**
* Count books
*/
public static function count_books($search = '') {
global $wpdb;
$table = BDM_Database::table('books');
$where = '1=1';
$params = [];
if (!empty($search)) {
$where .= ' AND title LIKE %s';
$params[] = '%' . $wpdb->esc_like($search) . '%';
}
$query = "SELECT COUNT(*) FROM {$table} WHERE {$where}";
if (!empty($params)) {
$query = $wpdb->prepare($query, ...$params);
}
return $wpdb->get_var($query);
}
/**
* Get rating for book
*/
public static function get_rating($book_id) {
$book = self::get_book($book_id);
if (!$book) {
return [
'amazon' => 0,
'goodreads' => 0,
];
}
return [
'amazon' => (float) $book->rating_amazon,
'goodreads' => (float) $book->rating_goodreads,
];
}
}

View File

@@ -0,0 +1,305 @@
<?php
/**
* Database management class
*
* @package BD_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
class BDM_Database {
/**
* Get table names with prefix
*/
public static function table($name) {
global $wpdb;
return $wpdb->prefix . 'bd_' . $name;
}
/**
* Create all database tables
*/
public function create_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
// Books table
$sql_books = "CREATE TABLE " . self::table('books') . " (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
title varchar(255) NOT NULL,
slug varchar(255) NOT NULL,
description longtext,
cover_image varchar(500),
isbn_ebook varchar(20),
isbn_paperback varchar(20),
isbn_hardcover varchar(20),
isbn_audio varchar(20),
asin_ebook varchar(20),
asin_paperback varchar(20),
asin_hardcover varchar(20),
asin_audio varchar(20),
amazon_asin varchar(20),
goodreads_id varchar(20),
rating_amazon decimal(3,2) DEFAULT 0.00,
rating_goodreads decimal(3,2) DEFAULT 0.00,
last_sync datetime,
series_id bigint(20) unsigned,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_slug (slug),
KEY idx_series_id (series_id)
) $charset_collate;";
// Book authors table
$sql_authors = "CREATE TABLE " . self::table('book_authors') . " (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
book_id bigint(20) unsigned NOT NULL,
author_name varchar(255) NOT NULL,
author_id bigint(20) unsigned,
goodreads_author_id varchar(20),
is_primary tinyint(1) DEFAULT 0,
sort_order int(11) DEFAULT 0,
PRIMARY KEY (id),
KEY idx_book_id (book_id),
KEY idx_author_id (author_id)
) $charset_collate;";
// Series table
$sql_series = "CREATE TABLE " . self::table('series') . " (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
description longtext,
cover_image varchar(500),
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) $charset_collate;";
// Retail sites table
$sql_sites = "CREATE TABLE " . self::table('retail_sites') . " (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
name varchar(100) NOT NULL,
url_pattern text NOT NULL,
icon varchar(255),
affiliate_param varchar(50),
affiliate_code varchar(100),
is_custom tinyint(1) DEFAULT 0,
is_active tinyint(1) DEFAULT 1,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) $charset_collate;";
// Retail links table
$sql_links = "CREATE TABLE " . self::table('retail_links') . " (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
book_id bigint(20) unsigned NOT NULL,
site_id bigint(20) unsigned NOT NULL,
format varchar(20) NOT NULL,
url text NOT NULL,
is_custom tinyint(1) DEFAULT 0,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_book_id (book_id),
KEY idx_site_id (site_id),
KEY idx_format (format)
) $charset_collate;";
// Custom shortcodes table
$sql_shortcodes = "CREATE TABLE " . self::table('shortcodes') . " (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
slug varchar(100) NOT NULL,
target_url text NOT NULL,
redirect_type varchar(10) DEFAULT '301',
description text,
click_count bigint(20) DEFAULT 0,
is_active tinyint(1) DEFAULT 1,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_slug (slug)
) $charset_collate;";
// Shortcode clicks table
$sql_clicks = "CREATE TABLE " . self::table('shortcode_clicks') . " (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
shortcode_id bigint(20) unsigned NOT NULL,
clicked_at datetime DEFAULT CURRENT_TIMESTAMP,
ip_address varchar(45) NOT NULL,
user_agent text,
referer varchar(500),
PRIMARY KEY (id),
KEY idx_shortcode_id (shortcode_id),
KEY idx_clicked_at (clicked_at)
) $charset_collate;";
// Book SEO table
$sql_seo = "CREATE TABLE " . self::table('book_seo') . " (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
book_id bigint(20) unsigned NOT NULL,
seo_title varchar(255),
seo_description text,
seo_keywords varchar(500),
og_title varchar(255),
og_description text,
og_image varchar(500),
created_at datetime DEFAULT CURRENT_TIMESTAMP,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_book_id (book_id)
) $charset_collate;";
// Authors table
$sql_authors_table = "CREATE TABLE " . self::table('authors') . " (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
display_name varchar(255) NOT NULL,
email varchar(100),
website varchar(500),
bio text,
goodreads_author_id varchar(20),
photo_url varchar(500),
created_at datetime DEFAULT CURRENT_TIMESTAMP,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) $charset_collate;";
dbDelta($sql_books);
dbDelta($sql_authors);
dbDelta($sql_series);
dbDelta($sql_sites);
dbDelta($sql_links);
dbDelta($sql_shortcodes);
dbDelta($sql_clicks);
dbDelta($sql_seo);
dbDelta($sql_authors_table);
// Migration: add asin columns if missing
$this->maybe_add_columns();
// Insert default retail sites
$this->insert_default_sites();
// Store schema version
update_option('bd_bookshelf_db_version', BD_BOOKSHELF_VERSION);
}
/**
* Add missing columns to existing tables
*/
private function maybe_add_columns() {
global $wpdb;
$table = self::table('books');
$columns = $wpdb->get_col("DESCRIBE {$table}", 0);
$missing = [
'asin_ebook' => "ALTER TABLE {$table} ADD COLUMN asin_ebook varchar(20) AFTER isbn_audio",
'asin_paperback' => "ALTER TABLE {$table} ADD COLUMN asin_paperback varchar(20) AFTER asin_ebook",
'asin_hardcover' => "ALTER TABLE {$table} ADD COLUMN asin_hardcover varchar(20) AFTER asin_paperback",
'asin_audio' => "ALTER TABLE {$table} ADD COLUMN asin_audio varchar(20) AFTER asin_hardcover",
];
foreach ($missing as $col => $sql) {
if (!in_array($col, $columns)) {
$wpdb->query($sql);
}
}
}
/**
* Insert default retail sites
*/
private function insert_default_sites() {
global $wpdb;
$table = self::table('retail_sites');
$default_sites = [
[
'name' => 'Amazon',
'url_pattern' => 'https://www.amazon.com/dp/{ISBN}',
'icon' => 'amazon',
'affiliate_param' => 'tag',
'affiliate_code' => '',
'is_custom' => 0,
'is_active' => 1,
],
[
'name' => 'Barnes & Noble',
'url_pattern' => 'https://www.barnesandnoble.com/w/?ean={ISBN}',
'icon' => 'barnes-noble',
'affiliate_param' => '',
'affiliate_code' => '',
'is_custom' => 0,
'is_active' => 1,
],
[
'name' => 'Apple Books',
'url_pattern' => 'https://books.apple.com/book/{ISBN}',
'icon' => 'apple',
'affiliate_param' => '',
'affiliate_code' => '',
'is_custom' => 0,
'is_active' => 1,
],
[
'name' => 'Google Play',
'url_pattern' => 'https://play.google.com/store/search?q={ISBN}&c=books',
'icon' => 'google',
'affiliate_param' => '',
'affiliate_code' => '',
'is_custom' => 0,
'is_active' => 1,
],
[
'name' => 'Kobo',
'url_pattern' => 'https://www.kobo.com/us/en/ebook/{ISBN}',
'icon' => 'kobo',
'affiliate_param' => '',
'affiliate_code' => '',
'is_custom' => 0,
'is_active' => 1,
],
];
foreach ($default_sites as $site) {
$exists = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$table} WHERE name = %s",
$site['name']
)
);
if (!$exists) {
$wpdb->insert($table, $site);
}
}
}
/**
* Drop all plugin tables
*/
public function drop_tables() {
global $wpdb;
$tables = [
'books',
'book_authors',
'authors',
'series',
'retail_sites',
'retail_links',
'shortcodes',
'shortcode_clicks',
'book_seo',
];
foreach ($tables as $table) {
$wpdb->query("DROP TABLE IF EXISTS " . self::table($table));
}
}
}

View File

@@ -0,0 +1,123 @@
<?php
/**
* GDPR Compliance class
*
* @package BD_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
class BDM_GDPR {
/**
* Constructor
*/
public function __construct() {
add_action('wp_footer', [$this, 'render_consent_banner']);
add_action('wp_enqueue_scripts', [$this, 'enqueue_scripts']);
// AJAX handlers
add_action('wp_ajax_bd_bookshelf_accept_consent', [$this, 'ajax_accept_consent']);
add_action('wp_ajax_nopriv_bd_bookshelf_accept_consent', [$this, 'ajax_accept_consent']);
}
/**
* Enqueue GDPR scripts
*/
public function enqueue_scripts() {
if (is_admin()) {
return;
}
wp_enqueue_script(
'bd-bookshelf-gdpr',
BD_BOOKSHELF_URL . 'public/js/bd-bookshelf-gdpr.js',
[],
BD_BOOKSHELF_VERSION,
true
);
wp_localize_script('bd-bookshelf-gdpr', 'bdBookshelfGdpr', [
'ajaxurl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('bd_bookshelf_gdpr_nonce'),
'cookieDays' => 30,
]);
}
/**
* Render consent banner
*/
public function render_consent_banner() {
if (is_admin()) {
return;
}
if ($this->has_consent()) {
return;
}
$privacy_page_url = $this->get_privacy_page_url();
$privacy_text = get_option('bd_bookshelf_gdpr_text',
'We use cookies for affiliate tracking only. By clicking "Accept", you agree to our use of cookies as described in our <a href="' . esc_url($privacy_page_url) . '" target="_blank">Privacy Policy</a>.'
);
echo '<div id="bd-bookshelf-gdpr-banner" class="bd-bookshelf-gdpr-banner">';
echo '<div class="bd-bookshelf-gdpr-content">';
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 '</div>';
echo '</div>';
}
/**
* Check if user has given consent
*/
public static function has_consent() {
if (isset($_COOKIE['bd_bookshelf_consent'])) {
return true;
}
return false;
}
/**
* AJAX handler for accepting consent
*/
public function ajax_accept_consent() {
check_ajax_referer('bd_bookshelf_gdpr_nonce', 'nonce');
// Set consent cookie (30 days)
setcookie('bd_bookshelf_consent', '1', time() + (30 * DAY_IN_SECONDS), '/');
wp_send_json_success(['message' => 'Consent accepted']);
}
/**
* Get privacy page URL
*/
private function get_privacy_page_url() {
$privacy_page_id = get_option('wp_page_for_privacy_policy');
if ($privacy_page_id) {
return get_permalink($privacy_page_id);
}
return '#';
}
/**
* Get GDPR consent banner text
*/
public static function get_consent_text() {
return get_option('bd_bookshelf_gdpr_text', '');
}
/**
* Update GDPR consent banner text
*/
public static function update_consent_text($text) {
update_option('bd_bookshelf_gdpr_text', sanitize_text_field($text));
}
}

View File

@@ -0,0 +1,322 @@
<?php
/**
* Goodreads Scraper class
*
* Fetches author and book data from Goodreads
*
* @package BD_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
class BDM_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';
/**
* Get author info from Goodreads
*/
public function get_author_info($author_id) {
$url = "https://www.goodreads.com/author/show/{$author_id}";
$response = wp_remote_get($url, [
'timeout' => 30,
'headers' => [
'User-Agent' => $this->user_agent,
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language' => 'en-US,en;q=0.5',
],
]);
if (is_wp_error($response)) {
return ['error' => $response->get_error_message()];
}
$body = wp_remote_retrieve_body($response);
return $this->parse_author_page($body, $author_id);
}
/**
* Get books from Goodreads author page
*/
public function get_author_books($author_id) {
$url = "https://www.goodreads.com/author/list/{$author_id}";
$response = wp_remote_get($url, [
'timeout' => 30,
'headers' => [
'User-Agent' => $this->user_agent,
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language' => 'en-US,en;q=0.5',
],
]);
if (is_wp_error($response)) {
return ['error' => $response->get_error_message()];
}
$body = wp_remote_retrieve_body($response);
return $this->parse_books_list($body);
}
/**
* Get full book details from individual book page
*/
public function get_book_details($goodreads_id) {
$url = "https://www.goodreads.com/book/show/{$goodreads_id}";
$response = wp_remote_get($url, [
'timeout' => 30,
'headers' => [
'User-Agent' => $this->user_agent,
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language' => 'en-US,en;q=0.5',
],
]);
if (is_wp_error($response)) {
return ['error' => $response->get_error_message()];
}
$body = wp_remote_retrieve_body($response);
return $this->parse_book_details($body, $goodreads_id);
}
/**
* Parse author page HTML
*/
private function parse_author_page($html, $author_id) {
$author = [
'id' => $author_id,
'name' => '',
'photo' => '',
'bio' => '',
'website' => '',
'genre' => '',
'books_count' => 0,
'average_rating' => 0,
];
// Extract author name
if (preg_match('/<h1[^>]*class="[^"]*authorName[^"]*"[^>]*>(.*?)<\/h1>/si', $html, $matches)) {
$author['name'] = trim(strip_tags($matches[1]));
}
// Extract photo
if (preg_match('/<img[^>]*class="[^"]*authorImage[^"]*"[^>]*src="([^"]+)"/si', $html, $matches)) {
$author['photo'] = $matches[1];
}
// Extract bio
if (preg_match('/<div[^>]*class="[^"]*authorAbout[^"]*"[^>]*>.*?<div[^>]*class="[^"]*aboutRight[^"]*"[^>]*>(.*?)<\/div>/si', $html, $matches)) {
$author['bio'] = trim(strip_tags($matches[1]));
}
// Extract website
if (preg_match('/<a[^>]*class="[^"]*authorLink[^"]*"[^>]*href="([^"]+)"/si', $html, $matches)) {
$author['website'] = $matches[1];
}
// Extract books count
if (preg_match('/(\d+)\s*books?/si', $html, $matches)) {
$author['books_count'] = absint($matches[1]);
}
return $author;
}
/**
* Parse books list HTML
*/
private function parse_books_list($html) {
$books = [];
// Match book rows
if (preg_match_all('/<tr[^>]*itemtype="http:\/\/schema.org\/Book"[^>]*>(.*?)<\/tr>/si', $html, $matches)) {
foreach ($matches[1] as $row) {
$book = $this->parse_book_row($row);
if ($book) {
$books[] = $book;
}
}
}
return $books;
}
/**
* Parse individual book row
*/
private function parse_book_row($html) {
$book = [
'title' => '',
'author' => '',
'isbn' => '',
'isbn13' => '',
'rating' => 0,
'ratings_count' => 0,
'reviews_count' => 0,
'cover_image' => '',
'description' => '',
'published' => '',
'goodreads_id' => '',
'goodreads_url' => '',
];
// Extract title from bookTitle span (role=heading)
if (preg_match('/<span[^>]*itemprop=["\']name["\'][^>]*role=["\']heading["\'][^>]*>(.*?)<\/span>/si', $html, $matches)) {
$book['title'] = trim(strip_tags($matches[1]));
} elseif (preg_match('/<a[^>]*class="[^"]*bookTitle[^"]*"[^>]*>.*?<span[^>]*>(.*?)<\/span>/si', $html, $matches)) {
$book['title'] = trim(strip_tags($matches[1]));
} elseif (preg_match('/<a[^>]*class="[^"]*bookTitle[^"]*"[^>]*>(.*?)<\/a>/si', $html, $matches)) {
$book['title'] = trim(strip_tags($matches[1]));
}
// Extract Goodreads URL and ID
if (preg_match('/<a[^>]*class="[^"]*bookTitle[^"]*"[^>]*href="([^"]+)"/si', $html, $matches)) {
$book['goodreads_url'] = trim($matches[1]);
if (preg_match('/\/book\/show\/(\d+)/', $matches[1], $id_matches)) {
$book['goodreads_id'] = $id_matches[1];
}
}
// Extract author
if (preg_match('/<a[^>]*class="[^"]*authorName[^"]*"[^>]*>.*?<span[^>]*itemprop=["\']name["\'][^>]*>(.*?)<\/span>/si', $html, $matches)) {
$book['author'] = trim(strip_tags($matches[1]));
} elseif (preg_match('/<a[^>]*class="[^"]*authorName[^"]*"[^>]*>(.*?)<\/a>/si', $html, $matches)) {
$book['author'] = trim(strip_tags($matches[1]));
}
// Extract cover image (class="bookCover" in list, class="bookImage" elsewhere)
if (preg_match('/<img[^>]*class="[^"]*bookCover[^"]*"[^>]*src="([^"]+)"/si', $html, $matches)) {
$book['cover_image'] = $matches[1];
} elseif (preg_match('/<img[^>]*class="[^"]*bookImage[^"]*"[^>]*src="([^"]+)"/si', $html, $matches)) {
$book['cover_image'] = $matches[1];
}
// Extract rating from minirating span
if (preg_match('/([\d.]+)\s*avg\s*rating\s*(?:&mdash;|—|--)\s*(\d[\d,]*)\s*rating/i', $html, $matches)) {
$book['rating'] = (float) $matches[1];
$book['ratings_count'] = absint(str_replace(',', '', $matches[2]));
} elseif (preg_match('/<span[^>]*class="[^"]*rating[^"]*"[^>]*>\s*(\d+\.?\d*)/si', $html, $matches)) {
$book['rating'] = (float) $matches[1];
}
// Extract ratings count (fallback)
if ($book['ratings_count'] === 0 && preg_match('/(\d+[\d,]*)\s*ratings?/si', $html, $matches)) {
$book['ratings_count'] = absint(str_replace(',', '', $matches[1]));
}
// Extract reviews count
if (preg_match('/(\d+[\d,]*)\s*reviews?/si', $html, $matches)) {
$book['reviews_count'] = absint(str_replace(',', '', $matches[1]));
}
// Extract ISBN
if (preg_match('/isbn(?:13)?[:\s]*(\d{10,13})/si', $html, $matches)) {
$book['isbn'] = $matches[1];
}
// Extract published date
if (preg_match('/published[^:]*:\s*([A-Za-z]+\s+\d{1,2},?\s*\d{4})/si', $html, $matches)) {
$book['published'] = trim($matches[1]);
}
// Upgrade cover image to larger version
if (!empty($book['cover_image'])) {
$book['cover_image'] = $this->upgrade_cover_url($book['cover_image']);
}
// Only return if we have a title
if (empty($book['title'])) {
return null;
}
return $book;
}
/**
* Parse book details page
*/
private function parse_book_details($html, $goodreads_id) {
$book = [
'goodreads_id' => $goodreads_id,
'title' => '',
'author' => '',
'isbn' => '',
'isbn13' => '',
'rating' => 0,
'ratings_count' => 0,
'reviews_count' => 0,
'cover_image' => '',
'description' => '',
'published' => '',
'pages' => 0,
'genre' => '',
];
// Extract title
if (preg_match('/<h1[^>]*id="bookTitle"[^>]*>(.*?)<\/h1>/si', $html, $matches)) {
$book['title'] = trim(strip_tags($matches[1]));
}
// Extract author
if (preg_match('/<a[^>]*class="[^"]*authorName[^"]*"[^>]*>(.*?)<\/a>/si', $html, $matches)) {
$book['author'] = trim(strip_tags($matches[1]));
}
// Extract cover image
if (preg_match('/<img[^>]*id="coverImage"[^>]*src="([^"]+)"/si', $html, $matches)) {
$book['cover_image'] = $matches[1];
}
// Extract description
if (preg_match('/<div[^>]*id="description"[^>]*>.*?<span[^>]*>(.*?)<\/span>/si', $html, $matches)) {
$book['description'] = trim(strip_tags($matches[1]));
}
// Extract ISBN13
if (preg_match('/isbn13[:\s]*(\d{13})/si', $html, $matches)) {
$book['isbn13'] = $matches[1];
}
// Extract ISBN
if (preg_match('/isbn[:\s]*(\d{10})/si', $html, $matches)) {
$book['isbn'] = $matches[1];
}
// Extract rating
if (preg_match('/<span[^>]*class="[^"]*RatingStatistics__rating[^"]*"[^>]*>(\d+\.?\d*)/si', $html, $matches)) {
$book['rating'] = (float) $matches[1];
}
// Extract pages
if (preg_match('/(\d+)\s*pages?/si', $html, $matches)) {
$book['pages'] = absint($matches[1]);
}
// Extract published date
if (preg_match('/published[^:]*:?\s*([A-Za-z]+\s+\d{1,2},?\s*\d{4})/si', $html, $matches)) {
$book['published'] = trim($matches[1]);
}
return $book;
}
/**
* Upgrade a Goodreads cover URL to a larger version
*
* Goodreads uses _SY75_ for thumbnails. We can change to _SX300_ for larger.
*/
private function upgrade_cover_url($url) {
// Replace thumbnail size with larger version
$url = preg_replace('/\._SY\d+_/', '._SX300_', $url);
$url = preg_replace('/\._SX\d+_/', '._SX300_', $url);
return $url;
}
}

View File

@@ -0,0 +1,271 @@
<?php
/**
* Migration Manager class
*
* @package BD_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
class BDM_Migration {
/**
* Constructor
*/
public function __construct() {
add_action('admin_menu', [$this, 'add_admin_menu']);
// AJAX handlers
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_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']
);
}
/**
* Render import page
*/
public function render_import_page() {
if (!current_user_can('manage_options')) {
wp_die(__('You do not have sufficient permissions to access this page.', 'bd-bookshelf'));
}
?>
<div class="wrap">
<h1><?php esc_html_e('Import Books', 'bd-bookshelf'); ?></h1>
<div class="bd-bookshelf-import-section">
<h2><?php esc_html_e('Import from WordPress Posts', 'bd-bookshelf'); ?></h2>
<p><?php esc_html_e('Import books from existing WordPress posts or custom post types.', 'bd-bookshelf'); ?></p>
<form id="bd-bookshelf-import-posts-form">
<?php wp_nonce_field('bd_bookshelf_import_posts', 'nonce'); ?>
<table class="form-table">
<tr>
<th><label for="post_type"><?php esc_html_e('Post Type', 'bd-bookshelf'); ?></label></th>
<td>
<select name="post_type" id="post_type">
<option value="post"><?php esc_html_e('Posts', 'bd-bookshelf'); ?></option>
<option value="page"><?php esc_html_e('Pages', 'bd-bookshelf'); ?></option>
<option value="book"><?php esc_html_e('Books (if available)', 'bd-bookshelf'); ?></option>
</select>
</td>
</tr>
<tr>
<th><label for="limit"><?php esc_html_e('Limit', 'bd-bookshelf'); ?></label></th>
<td>
<input type="number" name="limit" id="limit" value="100" min="1" max="1000">
</td>
</tr>
</table>
<p class="submit">
<button type="submit" class="button button-primary"><?php esc_html_e('Preview Import', 'bd-bookshelf'); ?></button>
</p>
</form>
<div id="bd-bookshelf-import-preview" style="display: none;">
<h3><?php esc_html_e('Preview', 'bd-bookshelf'); ?></h3>
<div id="bd-bookshelf-import-results"></div>
<p class="submit">
<button type="button" id="bd-bookshelf-run-import" class="button button-primary"><?php esc_html_e('Run Import', 'bd-bookshelf'); ?></button>
</p>
</div>
</div>
<div class="bd-bookshelf-import-section">
<h2><?php esc_html_e('Import from CSV', 'bd-bookshelf'); ?></h2>
<p><?php esc_html_e('Import books from a CSV file.', 'bd-bookshelf'); ?></p>
<form id="bd-bookshelf-import-csv-form" enctype="multipart/form-data">
<?php wp_nonce_field('bd_bookshelf_import_csv', 'nonce'); ?>
<table class="form-table">
<tr>
<th><label for="csv_file"><?php esc_html_e('CSV File', 'bd-bookshelf'); ?></label></th>
<td>
<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>
</td>
</tr>
</table>
<p class="submit">
<button type="submit" class="button button-primary"><?php esc_html_e('Import CSV', 'bd-bookshelf'); ?></button>
</p>
</form>
</div>
</div>
<?php
}
/**
* AJAX handler for previewing post import
*/
public function ajax_import_posts() {
check_ajax_referer('bd_bookshelf_import_posts', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Insufficient permissions']);
}
$post_type = sanitize_text_field($_POST['post_type'] ?? 'post');
$limit = absint($_POST['limit'] ?? 100);
$posts = get_posts([
'post_type' => $post_type,
'post_status' => 'publish',
'numberposts' => $limit,
]);
$preview = [];
foreach ($posts as $post) {
$preview[] = [
'id' => $post->ID,
'title' => $post->post_title,
'date' => $post->post_date,
'content' => wp_trim_words($post->post_content, 20),
];
}
wp_send_json_success(['posts' => $preview, 'count' => count($preview)]);
}
/**
* AJAX handler for running post import
*/
public function ajax_run_import_posts() {
check_ajax_referer('bd_bookshelf_import_posts', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Insufficient permissions']);
}
$post_type = sanitize_text_field($_POST['post_type'] ?? 'post');
$limit = absint($_POST['limit'] ?? 100);
$posts = get_posts([
'post_type' => $post_type,
'post_status' => 'publish',
'numberposts' => $limit,
]);
$imported = 0;
$skipped = 0;
foreach ($posts as $post) {
// Check if already imported
$existing = BDM_Book_Manager::get_book_by_title($post->post_title);
if ($existing) {
$skipped++;
continue;
}
// Import book
$book_id = BDM_Book_Manager::create_book([
'title' => $post->post_title,
'description' => $post->post_content,
]);
if ($book_id) {
// Link to WordPress post
update_post_meta($post->ID, '_bd_bookshelf_book_id', $book_id);
$imported++;
}
}
wp_send_json_success([
'message' => sprintf('Imported %d books, skipped %d', $imported, $skipped),
'imported' => $imported,
'skipped' => $skipped,
]);
}
/**
* AJAX handler for CSV import
*/
public function ajax_import_csv() {
check_ajax_referer('bd_bookshelf_import_csv', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Insufficient permissions']);
}
if (empty($_FILES['csv_file']['tmp_name'])) {
wp_send_json_error(['message' => 'No file uploaded']);
}
$file = $_FILES['csv_file']['tmp_name'];
$handle = fopen($file, 'r');
if (!$handle) {
wp_send_json_error(['message' => 'Unable to open file']);
}
// Get header row
$header = fgetcsv($handle);
$imported = 0;
$skipped = 0;
$errors = [];
while (($row = fgetcsv($handle)) !== false) {
$data = array_combine($header, $row);
if (empty($data['title'])) {
$errors[] = 'Row ' . ($imported + $skipped + 1) . ': Missing title';
continue;
}
// Check if already imported
$existing = BDM_Book_Manager::get_book_by_title($data['title']);
if ($existing) {
$skipped++;
continue;
}
// Create book
$book_id = BDM_Book_Manager::create_book([
'title' => $data['title'],
'description' => $data['description'] ?? '',
'isbn_ebook' => $data['isbn_ebook'] ?? '',
'isbn_paperback' => $data['isbn_paperback'] ?? '',
'isbn_hardcover' => $data['isbn_hardcover'] ?? '',
'isbn_audio' => $data['isbn_audio'] ?? '',
'amazon_asin' => $data['amazon_asin'] ?? '',
'goodreads_id' => $data['goodreads_id'] ?? '',
]);
if ($book_id) {
$imported++;
} else {
$errors[] = 'Row ' . ($imported + $skipped + 1) . ': Failed to create book';
}
}
fclose($handle);
wp_send_json_success([
'message' => sprintf('Imported %d books, skipped %d, %d errors', $imported, $skipped, count($errors)),
'imported' => $imported,
'skipped' => $skipped,
'errors' => $errors,
]);
}
}

View File

@@ -0,0 +1,213 @@
<?php
/**
* Rating Sync Engine class
*
* @package BD_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
class BDM_Rating_Sync {
/**
* Constructor
*/
public function __construct() {
// Schedule cron job
add_action('bd_bookshelf_sync_ratings', [$this, 'sync_all_ratings']);
// Register cron interval
add_filter('cron_schedules', [$this, 'add_cron_interval']);
// Schedule on plugin activation
if (!wp_next_scheduled('bd_bookshelf_sync_ratings')) {
wp_schedule_event(time(), 'bd_bookshelf_twelve_hours', 'bd_bookshelf_sync_ratings');
}
}
/**
* Add custom cron interval
*/
public function add_cron_interval($schedules) {
$schedules['bd_bookshelf_twelve_hours'] = [
'interval' => 43200, // 12 hours in seconds
'display' => esc_html__('Every 12 Hours', 'bd-bookshelf'),
];
return $schedules;
}
/**
* Sync all book ratings
*/
public function sync_all_ratings() {
$books = BDM_Book_Manager::get_books(['number' => 999]);
$synced = 0;
$failed = 0;
foreach ($books as $book) {
$result = $this->sync_book_rating($book->id);
if ($result) {
$synced++;
} else {
$failed++;
}
// Delay between requests to avoid rate limiting
sleep(2);
}
// Log results
update_option('bd_bookshelf_last_sync', [
'time' => current_time('mysql'),
'synced' => $synced,
'failed' => $failed,
'total' => count($books),
]);
return ['synced' => $synced, 'failed' => $failed];
}
/**
* Sync rating for a single book
*/
public function sync_book_rating($book_id) {
global $wpdb;
$book = BDM_Book_Manager::get_book($book_id);
if (!$book) {
return false;
}
$amazon_rating = 0;
$goodreads_rating = 0;
// Sync Amazon rating
if (!empty($book->amazon_asin) || !empty($book->isbn_ebook)) {
$isbn = $book->amazon_asin ?: $book->isbn_ebook;
$amazon_rating = $this->fetch_amazon_rating($isbn);
// Keep last rating if fetch fails
if ($amazon_rating === false) {
$amazon_rating = $book->rating_amazon;
}
}
// Sync Goodreads rating
if (!empty($book->goodreads_id)) {
$goodreads_rating = $this->fetch_goodreads_rating($book->goodreads_id);
// Keep last rating if fetch fails
if ($goodreads_rating === false) {
$goodreads_rating = $book->rating_goodreads;
}
}
// Update book ratings
$table = BDM_Database::table('books');
$wpdb->update(
$table,
[
'rating_amazon' => $amazon_rating,
'rating_goodreads' => $goodreads_rating,
'last_sync' => current_time('mysql'),
],
['id' => $book_id],
['%f', '%f', '%s'],
['%d']
);
return true;
}
/**
* Fetch Amazon rating via web scraping
*/
private function fetch_amazon_rating($isbn) {
$url = "https://www.amazon.com/dp/{$isbn}";
$response = wp_remote_get($url, [
'timeout' => 15,
'headers' => [
'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',
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language' => 'en-US,en;q=0.5',
],
]);
if (is_wp_error($response)) {
error_log('BD Bookshelf: Amazon fetch error - ' . $response->get_error_message());
return false;
}
$body = wp_remote_retrieve_body($response);
// Parse rating from HTML
// Amazon uses data-hook="review-star-rating" or similar
if (preg_match('/data-hook="review-star-rating"[^>]*>.*?(\d+\.?\d*)\s*out of\s*5/si', $body, $matches)) {
return (float) $matches[1];
}
// Alternative pattern
if (preg_match('/a-icon-alt[^>]*>(\d+\.?\d*)\s*out of\s*\d+ stars/si', $body, $matches)) {
return (float) $matches[1];
}
return false;
}
/**
* Fetch Goodreads rating via web scraping
*/
private function fetch_goodreads_rating($goodreads_id) {
$url = "https://www.goodreads.com/book/show/{$goodreads_id}";
$response = wp_remote_get($url, [
'timeout' => 15,
'headers' => [
'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',
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language' => 'en-US,en;q=0.5',
],
]);
if (is_wp_error($response)) {
error_log('BD Bookshelf: Goodreads fetch error - ' . $response->get_error_message());
return false;
}
$body = wp_remote_retrieve_body($response);
// Parse rating from HTML
// Goodreads uses ratingValue in meta or specific class
if (preg_match('/ratingValue[^>]*>(\d+\.?\d*)</si', $body, $matches)) {
return (float) $matches[1];
}
// Alternative pattern
if (preg_match('/class="RatingStatistics__rating"[^>]*>(\d+\.?\d*)</si', $body, $matches)) {
return (float) $matches[1];
}
return false;
}
/**
* Manual sync trigger
*/
public static function manual_sync() {
$sync = new self();
return $sync->sync_all_ratings();
}
/**
* Get last sync info
*/
public static function get_last_sync_info() {
return get_option('bd_bookshelf_last_sync', []);
}
}

View File

@@ -0,0 +1,295 @@
<?php
/**
* Retail Sites management class
*
* @package BD_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
class BDM_Retail_Sites {
/**
* Get all retail sites
*/
public static function get_sites($active_only = true) {
global $wpdb;
$table = BDM_Database::table('retail_sites');
$where = $active_only ? 'is_active = 1' : '1=1';
$sites = $wpdb->get_results(
"SELECT * FROM {$table} WHERE {$where} ORDER BY is_custom ASC, name ASC"
);
return $sites;
}
/**
* Get site by ID
*/
public static function get_site($site_id) {
global $wpdb;
$table = BDM_Database::table('retail_sites');
return $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$table} WHERE id = %d",
$site_id
)
);
}
/**
* Create new retail site
*/
public static function create_site($data) {
global $wpdb;
$table = BDM_Database::table('retail_sites');
$result = $wpdb->insert($table, [
'name' => sanitize_text_field($data['name']),
'url_pattern' => esc_url_raw($data['url_pattern']),
'icon' => sanitize_text_field($data['icon'] ?? ''),
'affiliate_param' => sanitize_text_field($data['affiliate_param'] ?? ''),
'affiliate_code' => sanitize_text_field($data['affiliate_code'] ?? ''),
'is_custom' => 1,
'is_active' => 1,
]);
return $result ? $wpdb->insert_id : false;
}
/**
* Update retail site
*/
public static function update_site($site_id, $data) {
global $wpdb;
$table = BDM_Database::table('retail_sites');
$update_data = [];
if (isset($data['name'])) {
$update_data['name'] = sanitize_text_field($data['name']);
}
if (isset($data['url_pattern'])) {
$update_data['url_pattern'] = esc_url_raw($data['url_pattern']);
}
if (isset($data['icon'])) {
$update_data['icon'] = sanitize_text_field($data['icon']);
}
if (isset($data['affiliate_param'])) {
$update_data['affiliate_param'] = sanitize_text_field($data['affiliate_param']);
}
if (isset($data['affiliate_code'])) {
$update_data['affiliate_code'] = sanitize_text_field($data['affiliate_code']);
}
if (isset($data['is_active'])) {
$update_data['is_active'] = absint($data['is_active']);
}
if (empty($update_data)) {
return false;
}
$result = $wpdb->update(
$table,
$update_data,
['id' => $site_id],
null,
['%d']
);
return $result !== false;
}
/**
* Delete retail site
*/
public static function delete_site($site_id) {
global $wpdb;
// Only allow deleting custom sites
$site = self::get_site($site_id);
if ($site && $site->is_custom) {
$wpdb->delete(BDM_Database::table('retail_links'), ['site_id' => $site_id], ['%d']);
return $wpdb->delete(BDM_Database::table('retail_sites'), ['id' => $site_id], ['%d']) !== false;
}
return false;
}
/**
* Generate links for a book
*/
public static function generate_links_for_book($book_id) {
global $wpdb;
$book = BDM_Book_Manager::get_book($book_id);
if (!$book) {
return false;
}
$sites = self::get_sites(true);
$links_table = BDM_Database::table('retail_links');
// Delete existing links
$wpdb->delete($links_table, ['book_id' => $book_id], ['%d']);
// Format to ISBN mapping
$formats = [
'ebook' => $book->isbn_ebook,
'paperback' => $book->isbn_paperback,
'hardcover' => $book->isbn_hardcover,
'audio' => $book->isbn_audio,
];
// Use Amazon ASIN for Amazon if available
$amazon_asin = $book->amazon_asin ?: '';
foreach ($formats as $format => $isbn) {
if (empty($isbn)) {
continue;
}
foreach ($sites as $site) {
$site_isbn = $isbn;
// Special handling for Amazon - needs ISBN-10 for /dp/ path
if ($site->name === 'Amazon') {
// Use ASIN if available (it's already ISBN-10 format)
if (!empty($amazon_asin)) {
$site_isbn = $amazon_asin;
$url = str_replace('{ISBN}', $site_isbn, $site->url_pattern);
} else {
// Try to convert ISBN-13 to ISBN-10
$isbn10 = self::convert_isbn13_to_isbn10($isbn);
if ($isbn10) {
$url = str_replace('{ISBN}', $isbn10, $site->url_pattern);
} else {
// Use search fallback for ISBNs that can't convert (979 prefix)
$url = 'https://www.amazon.com/s?k=' . urlencode($isbn) . '&i=stripbooks';
}
}
// Add affiliate code if available
if (!empty($site->affiliate_param) && !empty($site->affiliate_code)) {
$separator = strpos($url, '?') !== false ? '&' : '?';
$url .= $separator . $site->affiliate_param . '=' . $site->affiliate_code;
}
$wpdb->insert($links_table, [
'book_id' => $book_id,
'site_id' => $site->id,
'format' => $format,
'url' => $url,
'is_custom' => 0,
]);
continue;
}
$url = str_replace('{ISBN}', $site_isbn, $site->url_pattern);
// Add affiliate code if available
if (!empty($site->affiliate_param) && !empty($site->affiliate_code)) {
$separator = strpos($url, '?') !== false ? '&' : '?';
$url .= $separator . $site->affiliate_param . '=' . $site->affiliate_code;
}
$wpdb->insert($links_table, [
'book_id' => $book_id,
'site_id' => $site->id,
'format' => $format,
'url' => $url,
'is_custom' => 0,
]);
}
}
return true;
}
/**
* Convert ISBN-13 to ISBN-10
*
* @param string $isbn13 ISBN-13 to convert.
* @return string|false ISBN-10 on success, false on failure.
*/
private static function convert_isbn13_to_isbn10($isbn13) {
// Clean the ISBN
$isbn13 = preg_replace('/[^0-9X]/i', '', $isbn13);
// Must be 13 digits
if (strlen($isbn13) !== 13) {
return false;
}
// Must start with 978 (Bookland prefix)
if (substr($isbn13, 0, 3) !== '978') {
return false;
}
// Take the first 9 digits (drop 978 prefix and last check digit)
$isbn10 = substr($isbn13, 3, 9);
// Calculate check digit
$sum = 0;
for ($i = 0; $i < 9; $i++) {
$sum += (int)$isbn10[$i] * (10 - $i);
}
$remainder = $sum % 11;
$check_digit = (11 - $remainder) % 11;
// Convert check digit to ISBN-10 format (10 = X)
$check_digit = ($check_digit === 10) ? 'X' : (string)$check_digit;
return $isbn10 . $check_digit;
}
/**
* Update custom link
*/
public static function update_link($link_id, $url) {
global $wpdb;
$table = BDM_Database::table('retail_links');
return $wpdb->update(
$table,
['url' => esc_url_raw($url), 'is_custom' => 1],
['id' => $link_id],
['%s', '%d'],
['%d']
) !== false;
}
/**
* Create custom link
*/
public static function create_custom_link($book_id, $site_id, $format, $url) {
global $wpdb;
$table = BDM_Database::table('retail_links');
$result = $wpdb->insert($table, [
'book_id' => $book_id,
'site_id' => $site_id,
'format' => $format,
'url' => esc_url_raw($url),
'is_custom' => 1,
]);
return $result ? $wpdb->insert_id : false;
}
}

View File

@@ -0,0 +1,223 @@
<?php
/**
* SEO Metadata Manager class
*
* @package BD_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
class BDM_SEO {
/**
* Constructor
*/
public function __construct() {
add_action('wp_head', [$this, 'output_meta_tags'], 1);
add_action('wp_head', [$this, 'output_open_graph'], 2);
add_action('wp_head', [$this, 'output_twitter_cards'], 3);
// Add sitemap entries
add_filter('wp_sitemaps_posts_entry', [$this, 'filter_sitemap_entry'], 10, 2);
}
/**
* Get book SEO data
*/
public static function get_book_seo($book_id) {
global $wpdb;
$table = BDM_Database::table('book_seo');
$seo = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$table} WHERE book_id = %d",
$book_id
)
);
return $seo;
}
/**
* Update book SEO data
*/
public static function update_book_seo($book_id, $data) {
global $wpdb;
$table = BDM_Database::table('book_seo');
$existing = self::get_book_seo($book_id);
$update_data = [
'book_id' => absint($book_id),
'seo_title' => sanitize_text_field($data['seo_title'] ?? ''),
'seo_description' => wp_kses_post($data['seo_description'] ?? ''),
'seo_keywords' => sanitize_text_field($data['seo_keywords'] ?? ''),
'og_title' => sanitize_text_field($data['og_title'] ?? ''),
'og_description' => wp_kses_post($data['og_description'] ?? ''),
'og_image' => esc_url_raw($data['og_image'] ?? ''),
];
if ($existing) {
$result = $wpdb->update(
$table,
$update_data,
['book_id' => $book_id],
null,
['%d']
);
} else {
$result = $wpdb->insert($table, $update_data);
}
return $result !== false;
}
/**
* Delete book SEO data
*/
public static function delete_book_seo($book_id) {
global $wpdb;
$table = BDM_Database::table('book_seo');
return $wpdb->delete($table, ['book_id' => $book_id], ['%d']) !== false;
}
/**
* Output meta tags
*/
public function output_meta_tags() {
if (!is_singular()) {
return;
}
$post_id = get_queried_object_id();
$book_id = $this->get_book_id_for_post($post_id);
if (!$book_id) {
return;
}
$seo = self::get_book_seo($book_id);
if (!$seo) {
return;
}
if (!empty($seo->seo_title)) {
echo '<meta name="title" content="' . esc_attr($seo->seo_title) . '">' . "\n";
}
if (!empty($seo->seo_description)) {
echo '<meta name="description" content="' . esc_attr($seo->seo_description) . '">' . "\n";
}
if (!empty($seo->seo_keywords)) {
echo '<meta name="keywords" content="' . esc_attr($seo->seo_keywords) . '">' . "\n";
}
}
/**
* Output Open Graph tags
*/
public function output_open_graph() {
if (!is_singular()) {
return;
}
$post_id = get_queried_object_id();
$book_id = $this->get_book_id_for_post($post_id);
if (!$book_id) {
return;
}
$seo = self::get_book_seo($book_id);
$book = BDM_Book_Manager::get_book($book_id);
if (!$book) {
return;
}
$og_title = !empty($seo->og_title) ? $seo->og_title : get_the_title($post_id);
$og_description = !empty($seo->og_description) ? $seo->og_description : wp_trim_words(get_the_excerpt($post_id), 30);
$og_image = !empty($seo->og_image) ? $seo->og_image : get_the_post_thumbnail_url($post_id, 'large');
echo '<meta property="og:type" content="book">' . "\n";
echo '<meta property="og:title" content="' . esc_attr($og_title) . '">' . "\n";
echo '<meta property="og:description" content="' . esc_attr($og_description) . '">' . "\n";
echo '<meta property="og:url" content="' . esc_url(get_permalink($post_id)) . '">' . "\n";
if ($og_image) {
echo '<meta property="og:image" content="' . esc_url($og_image) . '">' . "\n";
}
}
/**
* Output Twitter Card tags
*/
public function output_twitter_cards() {
if (!is_singular()) {
return;
}
$post_id = get_queried_object_id();
$book_id = $this->get_book_id_for_post($post_id);
if (!$book_id) {
return;
}
$seo = self::get_book_seo($book_id);
$twitter_title = !empty($seo->og_title) ? $seo->og_title : get_the_title($post_id);
$twitter_description = !empty($seo->og_description) ? $seo->og_description : wp_trim_words(get_the_excerpt($post_id), 30);
$twitter_image = !empty($seo->og_image) ? $seo->og_image : get_the_post_thumbnail_url($post_id, 'large');
echo '<meta name="twitter:card" content="summary_large_image">' . "\n";
echo '<meta name="twitter:title" content="' . esc_attr($twitter_title) . '">' . "\n";
echo '<meta name="twitter:description" content="' . esc_attr($twitter_description) . '">' . "\n";
if ($twitter_image) {
echo '<meta name="twitter:image" content="' . esc_url($twitter_image) . '">' . "\n";
}
}
/**
* Get book ID for a WordPress post
*/
private function get_book_id_for_post($post_id) {
// Check if there's a book ID stored in post meta
return get_post_meta($post_id, '_bd_bookshelf_book_id', true);
}
/**
* Filter sitemap entry for book posts
*/
public function filter_sitemap_entry($entry, $post) {
$book_id = $this->get_book_id_for_post($post->ID);
if (!$book_id) {
return $entry;
}
$book = BDM_Book_Manager::get_book($book_id);
if (!$book) {
return $entry;
}
// Higher rating = higher priority
$rating = max($book->rating_amazon, $book->rating_goodreads);
$priority = 0.5 + ($rating / 10); // 0.5 to 1.0 based on rating
$entry['loc'] = get_permalink($post->ID);
$entry['lastmod'] = get_the_modified_date('c', $post);
$entry['changefreq'] = 'monthly';
$entry['priority'] = round($priority, 1);
return $entry;
}
}

View File

@@ -0,0 +1,143 @@
<?php
/**
* Series Management class
*
* @package BD_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
class BDM_Series {
/**
* Get all series
*/
public static function get_series($args = []) {
global $wpdb;
$table = BDM_Database::table('series');
$defaults = [
'number' => 20,
'offset' => 0,
'orderby' => 'name',
'order' => 'ASC',
];
$args = wp_parse_args($args, $defaults);
$order = strtoupper($args['order']) === 'DESC' ? 'DESC' : 'ASC';
$query = $wpdb->prepare(
"SELECT * FROM {$table} ORDER BY {$args['orderby']} {$order} LIMIT %d OFFSET %d",
$args['number'],
$args['offset']
);
return $wpdb->get_results($query);
}
/**
* Get series by ID
*/
public static function get_series_by_id($series_id) {
global $wpdb;
$table = BDM_Database::table('series');
return $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$table} WHERE id = %d",
$series_id
)
);
}
/**
* Create series
*/
public static function create_series($data) {
global $wpdb;
$table = BDM_Database::table('series');
$result = $wpdb->insert($table, [
'name' => sanitize_text_field($data['name']),
'description' => wp_kses_post($data['description'] ?? ''),
'cover_image' => esc_url_raw($data['cover_image'] ?? ''),
]);
return $result ? $wpdb->insert_id : false;
}
/**
* Update series
*/
public static function update_series($series_id, $data) {
global $wpdb;
$table = BDM_Database::table('series');
$update_data = [];
if (isset($data['name'])) {
$update_data['name'] = sanitize_text_field($data['name']);
}
if (isset($data['description'])) {
$update_data['description'] = wp_kses_post($data['description']);
}
if (isset($data['cover_image'])) {
$update_data['cover_image'] = esc_url_raw($data['cover_image']);
}
if (empty($update_data)) {
return false;
}
$result = $wpdb->update(
$table,
$update_data,
['id' => $series_id],
null,
['%d']
);
return $result !== false;
}
/**
* Delete series
*/
public static function delete_series($series_id) {
global $wpdb;
// Unlink books from series
$books_table = BDM_Database::table('books');
$wpdb->update(
$books_table,
['series_id' => 0],
['series_id' => $series_id],
['%d'],
['%d']
);
// Delete series
$table = BDM_Database::table('series');
return $wpdb->delete($table, ['id' => $series_id], ['%d']) !== false;
}
/**
* Count series
*/
public static function count_series() {
global $wpdb;
$table = BDM_Database::table('series');
return $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
}
}

View File

@@ -0,0 +1,335 @@
<?php
/**
* Shortcodes Manager class
*
* @package BD_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
class BDM_Shortcodes {
/**
* Constructor
*/
public function __construct() {
// Register shortcodes
add_shortcode('sell_ebook', [$this, 'shortcode_sell_ebook']);
add_shortcode('sell_pb', [$this, 'shortcode_sell_pb']);
add_shortcode('sell_hc', [$this, 'shortcode_sell_hc']);
add_shortcode('sell_audio', [$this, 'shortcode_sell_audio']);
add_shortcode('rating', [$this, 'shortcode_rating']);
// Register redirect handler
add_action('template_redirect', [$this, 'handle_redirect']);
}
/**
* Shortcode: [sell_ebook id="123"]
*/
public function shortcode_sell_ebook($atts) {
return $this->render_sell_grid($atts, 'ebook');
}
/**
* Shortcode: [sell_pb id="123"]
*/
public function shortcode_sell_pb($atts) {
return $this->render_sell_grid($atts, 'paperback');
}
/**
* Shortcode: [sell_hc id="123"]
*/
public function shortcode_sell_hc($atts) {
return $this->render_sell_grid($atts, 'hardcover');
}
/**
* Shortcode: [sell_audio id="123"]
*/
public function shortcode_sell_audio($atts) {
return $this->render_sell_grid($atts, 'audio');
}
/**
* Render sell grid for a format
*/
private function render_sell_grid($atts, $format) {
$atts = shortcode_atts([
'id' => 0,
'class' => '',
'columns' => 'auto',
'show_icons' => 'true',
'show_names' => 'true',
'icon_style' => 'simple',
], $atts);
$book_id = absint($atts['id']);
if (!$book_id) {
return '';
}
$links = BDM_Book_Manager::get_book_retail_links($book_id, $format);
if (empty($links)) {
return '';
}
$class = sanitize_html_class('bd-bookshelf-sell-grid bd-bookshelf-format-' . $format . ' ' . $atts['class']);
$columns = in_array($atts['columns'], ['2', '3', '4']) ? absint($atts['columns']) : '';
$show_icons = $atts['show_icons'] === 'true';
$show_names = $atts['show_names'] === 'true';
$output = '<div class="' . esc_attr($class) . '"';
if ($columns) {
$output .= ' style="grid-template-columns: repeat(' . $columns . ', 1fr);"';
}
$output .= '>';
foreach ($links as $link) {
$output .= '<a href="' . esc_url($link->url) . '" class="bd-bookshelf-sell-item" target="_blank" rel="nofollow noopener">';
if ($show_icons) {
$output .= $this->render_site_icon($link->site_icon, $link->site_name, $atts['icon_style']);
}
if ($show_names) {
$output .= '<span class="bd-bookshelf-site-name">' . esc_html($link->site_name) . '</span>';
}
$output .= '</a>';
}
$output .= '</div>';
return $output;
}
/**
* Render site icon
*/
private function render_site_icon($icon, $name, $style = 'simple') {
if ($style === 'uploaded' && preg_match('/^https?:\/\//', $icon)) {
return '<img src="' . esc_url($icon) . '" alt="' . esc_attr($name) . '" class="bd-bookshelf-site-icon">';
}
// Simple letter icon
$letter = strtoupper(substr($name, 0, 1));
return '<span class="bd-bookshelf-site-icon bd-bookshelf-icon-simple">' . esc_html($letter) . '</span>';
}
/**
* Shortcode: [rating title="Book Title" format="both"]
*/
public function shortcode_rating($atts) {
$atts = shortcode_atts([
'title' => '',
'format' => 'both',
], $atts);
if (empty($atts['title'])) {
return '';
}
$book = BDM_Book_Manager::get_book_by_title(sanitize_text_field($atts['title']));
if (!$book) {
return '';
}
$rating = BDM_Book_Manager::get_rating($book->id);
$format = in_array($atts['format'], ['stars', 'numeric', 'both']) ? $atts['format'] : 'both';
$output = '<span class="bd-bookshelf-rating">';
if ($format === 'stars' || $format === 'both') {
$output .= $this->render_stars($rating['goodreads']);
}
if ($format === 'numeric' || $format === 'both') {
$output .= '<span class="bd-bookshelf-rating-number">' . number_format($rating['goodreads'], 1) . '/5</span>';
}
$output .= '</span>';
return $output;
}
/**
* Render star rating with Unicode stars
*/
private function render_stars($rating) {
$full_stars = floor($rating);
$half_star = ($rating - $full_stars) >= 0.5;
$empty_stars = 5 - $full_stars - ($half_star ? 1 : 0);
$output = '<span class="bd-bookshelf-stars">';
for ($i = 0; $i < $full_stars; $i++) {
$output .= '<span class="bd-bookshelf-star full">★</span>';
}
if ($half_star) {
$output .= '<span class="bd-bookshelf-star half">★</span>';
}
for ($i = 0; $i < $empty_stars; $i++) {
$output .= '<span class="bd-bookshelf-star empty">☆</span>';
}
$output .= '</span>';
return $output;
}
/**
* Handle shortcode redirects
*/
public function handle_redirect() {
global $wpdb;
if (is_admin()) {
return;
}
$slug = get_query_var('bd_bookshelf_redirect');
if (empty($slug)) {
return;
}
$table = BDM_Database::table('shortcodes');
$shortcode = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$table} WHERE slug = %s AND is_active = 1",
$slug
)
);
if (!$shortcode) {
return;
}
// Track click
$this->track_click($shortcode->id);
// Redirect
$type = in_array($shortcode->redirect_type, ['301', '302', '307']) ? $shortcode->redirect_type : '301';
wp_redirect($shortcode->target_url, absint($type));
exit;
}
/**
* Track click for shortcode
*/
private function track_click($shortcode_id) {
global $wpdb;
$table = BDM_Database::table('shortcode_clicks');
$wpdb->insert($table, [
'shortcode_id' => $shortcode_id,
'ip_address' => $this->get_client_ip(),
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : '',
'referer' => isset($_SERVER['HTTP_REFERER']) ? esc_url_raw($_SERVER['HTTP_REFERER']) : '',
]);
// Increment click count
$wpdb->query(
$wpdb->prepare(
"UPDATE " . BDM_Database::table('shortcodes') . " SET click_count = click_count + 1 WHERE id = %d",
$shortcode_id
)
);
}
/**
* Get client IP address
*/
private function get_client_ip() {
$ip = '';
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
$ip = $_SERVER['REMOTE_ADDR'];
}
return sanitize_text_field($ip);
}
/**
* Create custom shortcode
*/
public static function create_shortcode($slug, $target_url, $redirect_type = '301', $description = '') {
global $wpdb;
$table = BDM_Database::table('shortcodes');
$result = $wpdb->insert($table, [
'slug' => sanitize_title($slug),
'target_url' => esc_url_raw($target_url),
'redirect_type' => in_array($redirect_type, ['301', '302', '307']) ? $redirect_type : '301',
'description' => sanitize_text_field($description),
'is_active' => 1,
]);
return $result ? $wpdb->insert_id : false;
}
/**
* Update custom shortcode
*/
public static function update_shortcode($shortcode_id, $data) {
global $wpdb;
$table = BDM_Database::table('shortcodes');
$update_data = [];
if (isset($data['slug'])) {
$update_data['slug'] = sanitize_title($data['slug']);
}
if (isset($data['target_url'])) {
$update_data['target_url'] = esc_url_raw($data['target_url']);
}
if (isset($data['redirect_type'])) {
$update_data['redirect_type'] = in_array($data['redirect_type'], ['301', '302', '307']) ? $data['redirect_type'] : '301';
}
if (isset($data['description'])) {
$update_data['description'] = sanitize_text_field($data['description']);
}
if (isset($data['is_active'])) {
$update_data['is_active'] = absint($data['is_active']);
}
if (empty($update_data)) {
return false;
}
$result = $wpdb->update(
$table,
$update_data,
['id' => $shortcode_id],
null,
['%d']
);
return $result !== false;
}
/**
* Delete custom shortcode
*/
public static function delete_shortcode($shortcode_id) {
global $wpdb;
$wpdb->delete(BDM_Database::table('shortcode_clicks'), ['shortcode_id' => $shortcode_id], ['%d']);
return $wpdb->delete(BDM_Database::table('shortcodes'), ['id' => $shortcode_id], ['%d']) !== false;
}
}

View File

@@ -0,0 +1,214 @@
<?php
/**
* Theme Compatibility Layer
*
* Provides compatibility with BD BookHelper plugin interface
* so the BD Bookshelf theme works with our new plugin.
*
* @package BD_Bookshelf
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* BD_BookShelfHelper class for theme compatibility
* Maps theme expectations to our new plugin methods
*/
class BD_BookShelfHelper {
/**
* Get book cover URL
*/
public static function get_cover_url($book_id) {
$book = self::get_book_from_post($book_id);
if ($book && !empty($book->cover_image)) {
return $book->cover_image;
}
// Fallback to post thumbnail
$thumb = get_the_post_thumbnail_url($book_id, 'book-cover');
return $thumb ? $thumb : '';
}
/**
* Get pen name (primary author)
*/
public static function get_pen_name($book_id) {
$book = self::get_book_from_post($book_id);
if ($book && !empty($book->authors)) {
foreach ($book->authors as $author) {
if ($author->is_primary) {
return $author->author_name;
}
}
// Return first author if no primary
return $book->authors[0]->author_name;
}
return get_option('bdm_author_name', '');
}
/**
* Get ratings
*/
public static function get_ratings($book_id) {
$book = self::get_book_from_post($book_id);
if ($book) {
return [
'amazon' => (float) $book->rating_amazon,
'goodreads' => (float) $book->rating_goodreads,
];
}
return ['amazon' => 0, 'goodreads' => 0];
}
/**
* Get retailer links
*/
public static function get_retailer_links($book_id, $format = 'ebook') {
$book = self::get_book_from_post($book_id);
if (!$book) {
return [];
}
$links = BDM_Book_Manager::get_book_retail_links($book->id, $format);
$retailers = [];
foreach ($links as $link) {
$retailers[] = [
'url' => $link->url,
'name' => $link->site_name,
'retailer' => strtolower(str_replace(' ', '-', $link->site_name)),
];
}
return $retailers;
}
/**
* Get retailer links by language
*/
public static function get_retailer_links_by_language($book_id, $language = 'en', $format = 'ebook') {
return self::get_retailer_links($book_id, $format);
}
/**
* Get series book count
*/
public static function get_series_book_count($series_id) {
$books = BDM_Book_Manager::get_books_by_series($series_id);
return count($books);
}
/**
* Get series info
*/
public static function get_series_info($series_id) {
$series = BDM_Series::get_series_by_id($series_id);
if ($series) {
return [
'name' => $series->name,
'description' => $series->description,
];
}
return null;
}
/**
* Get reviews
*/
public static function get_reviews($book_id) {
// Return empty for now - reviews would need a separate implementation
return [];
}
/**
* Get pull quotes
*/
public static function get_pull_quotes($book_id) {
return [];
}
/**
* Get preview excerpt
*/
public static function get_preview_excerpt($book_id) {
$book = self::get_book_from_post($book_id);
if ($book) {
return wp_trim_words($book->description, 50);
}
return '';
}
/**
* Get available languages
*/
public static function get_available_languages($book_id) {
return ['en'];
}
/**
* Get awards
*/
public static function get_awards() {
return [];
}
/**
* Display retailer links
*/
public static function display_retailer_links($book_id, $language = 'en', $format = 'ebook') {
$links = self::get_retailer_links($book_id, $format);
if (empty($links)) {
return;
}
echo '<div class="retailer-links">';
foreach ($links as $link) {
echo '<a href="' . esc_url($link['url']) . '" target="_blank" rel="nofollow noopener" class="retailer-link">';
echo esc_html($link['name']);
echo '</a>';
}
echo '</div>';
}
/**
* Get book from post ID
* Maps WordPress post to our database book
*/
private static function get_book_from_post($post_id) {
global $wpdb;
// Check if post_id is actually a book ID from our database
$book = BDM_Book_Manager::get_book($post_id);
if ($book) {
return $book;
}
// Check if there's a book ID stored in post meta
$book_id = get_post_meta($post_id, '_bd_bookshelf_book_id', true);
if ($book_id) {
return BDM_Book_Manager::get_book($book_id);
}
// Try to find by title
$post = get_post($post_id);
if ($post) {
$book = BDM_Book_Manager::get_book_by_title($post->post_title);
if ($book) {
// Link post to book
update_post_meta($post_id, '_bd_bookshelf_book_id', $book->id);
return $book;
}
}
return null;
}
}

View File

@@ -0,0 +1,152 @@
/**
* BD Bookshelf - Public Styles
*
* @package BD_Bookshelf
*/
/* Sell Grid */
.bd-bookshelf-sell-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 15px;
margin: 20px 0;
}
.bd-bookshelf-sell-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 15px;
border: 1px solid #ddd;
border-radius: 8px;
text-decoration: none;
color: #333;
transition: all 0.3s ease;
background: #fff;
}
.bd-bookshelf-sell-item:hover {
border-color: #0073aa;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.bd-bookshelf-site-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
}
.bd-bookshelf-site-icon img {
max-width: 100%;
max-height: 100%;
}
.bd-bookshelf-icon-simple {
background: #0073aa;
color: #fff;
border-radius: 50%;
font-weight: bold;
font-size: 18px;
}
.bd-bookshelf-site-name {
font-size: 14px;
text-align: center;
}
/* Rating */
.bd-bookshelf-rating {
display: inline-flex;
align-items: center;
gap: 8px;
}
.bd-bookshelf-stars {
display: inline-flex;
gap: 2px;
}
.bd-bookshelf-star {
font-size: 18px;
}
.bd-bookshelf-star.full {
color: #f4b400;
}
.bd-bookshelf-star.half {
color: #f4b400;
position: relative;
}
.bd-bookshelf-star.empty {
color: #ccc;
}
.bd-bookshelf-rating-number {
font-size: 14px;
color: #666;
}
/* GDPR Banner */
.bd-bookshelf-gdpr-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #333;
color: #fff;
padding: 20px;
z-index: 9999;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.2);
}
.bd-bookshelf-gdpr-content {
max-width: 1200px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
}
.bd-bookshelf-gdpr-content p {
margin: 0;
flex: 1;
}
.bd-bookshelf-gdpr-content a {
color: #fff;
text-decoration: underline;
}
.bd-bookshelf-gdpr-accept {
background: #0073aa;
color: #fff;
border: none;
padding: 10px 25px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
}
.bd-bookshelf-gdpr-accept:hover {
background: #005a87;
}
/* Responsive */
@media (max-width: 768px) {
.bd-bookshelf-sell-grid {
grid-template-columns: repeat(2, 1fr);
}
.bd-bookshelf-gdpr-content {
flex-direction: column;
text-align: center;
}
}

View File

@@ -0,0 +1,67 @@
/**
* BD Bookshelf - GDPR Consent
*
* @package BD_Bookshelf
*/
(function($) {
'use strict';
$(document).ready(function() {
// Check if consent already given
if (bdBookshelfHasConsent()) {
return;
}
// Show banner
$('#bd-bookshelf-gdpr-banner').show();
// Handle accept button
$('#bd-bookshelf-gdpr-accept').on('click', function() {
$.ajax({
url: bdBookshelfGdpr.ajaxurl,
type: 'POST',
data: {
action: 'bd_bookshelf_accept_consent',
nonce: bdBookshelfGdpr.nonce
},
success: function(response) {
if (response.success) {
// Set cookie
bdBookshelfSetCookie('bd_bookshelf_consent', '1', bdBookshelfGdpr.cookieDays);
// Hide banner
$('#bd-bookshelf-gdpr-banner').fadeOut(300);
}
}
});
});
});
function bdBookshelfHasConsent() {
return bdBookshelfGetCookie('bd_bookshelf_consent') === '1';
}
function bdBookshelfSetCookie(name, value, days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
var expires = "expires=" + date.toUTCString();
document.cookie = name + "=" + value + ";" + expires + ";path=/";
}
function bdBookshelfGetCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(nameEQ) == 0) {
return c.substring(nameEQ.length, c.length);
}
}
return null;
}
})(jQuery);

View File

@@ -0,0 +1,14 @@
/**
* BD Bookshelf - Public JavaScript
*
* @package BD_Bookshelf
*/
(function($) {
'use strict';
$(document).ready(function() {
// Initialize any public-facing functionality
});
})(jQuery);

View File

@@ -0,0 +1,77 @@
=== 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

@@ -0,0 +1,27 @@
<?php
/**
* 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_%'");

BIN
bd-bookshelf-v2.2.0.zip Normal file

Binary file not shown.

BIN
bd-bookshelf-v2.2.10.zip Normal file

Binary file not shown.

BIN
bd-bookshelf.zip Normal file

Binary file not shown.

BIN
bd-bookshelf.zip.zip Executable file

Binary file not shown.

27
bd-bookshelf/404.php Normal file
View File

@@ -0,0 +1,27 @@
<?php
/**
* Template Name: 404 Error
*
* @package BD_Murphy
*/
get_header();
?>
<div class="error-container">
<div class="error-content">
<h1 class="error-code">404</h1>
<h2 class="error-title"><?php esc_html_e('Page Not Found', 'bd-bookshelf'); ?></h2>
<p class="error-message">
<?php esc_html_e('The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.', 'bd-bookshelf'); ?>
</p>
<p class="error-actions">
<a href="<?php echo esc_url(home_url('/')); ?>" class="btn-home">
<?php esc_html_e('Go to Homepage', 'bd-bookshelf'); ?>
</a>
</p>
</div>
</div>
<?php
get_footer();

View File

@@ -0,0 +1,151 @@
<?php
/**
* Template Name: All Books
*
* @package BD_Murphy
*/
get_header();
?>
<div class="page-container with-sidebar">
<div class="page-content">
<div class="books-container">
<header class="page-header">
<h1 class="page-title"><?php esc_html_e('Books', 'bd-bookshelf'); ?></h1>
<?php
if (function_exists('bdm_series_exist') && bdm_series_exist()) {
printf(
'<a href="%s" class="browse-series-link">%s</a>',
esc_url(home_url('/series/')),
esc_html__('Browse by Series', 'bd-bookshelf')
);
}
?>
</header>
<div class="books-list">
<?php
if (have_posts()) :
while (have_posts()) :
the_post();
setup_postdata($post);
$book_id = get_the_ID();
$cover_url = '';
if (has_post_thumbnail($book_id)) {
$cover_url = get_the_post_thumbnail_url($book_id, 'book-cover');
}
$title = esc_html(get_the_title());
$author = 'B.D. Murphy';
if (class_exists('BD_BookShelfHelper')) {
$author = BD_BookShelfHelper::get_pen_name($book_id);
}
$ratings = array();
$amazon = get_post_meta($book_id, 'amazon_rating', true);
$goodreads = get_post_meta($book_id, 'goodreads_rating', true);
if ($amazon) $ratings['amazon'] = $amazon;
if ($goodreads) $ratings['goodreads'] = $goodreads;
$blurb = '';
$excerpt = get_post_field('post_excerpt', $book_id);
if ($excerpt) {
$blurb = $excerpt;
} else {
$content = get_post_field('post_content', $book_id);
$blurb = wp_trim_words($content, 25, '...');
}
$series = null;
$terms = get_the_terms($book_id, 'series');
if ($terms && !is_wp_error($terms)) {
$series = array(
'name' => $terms[0]->name,
'slug' => $terms[0]->slug,
);
}
?>
<article class="book-entry" id="book-<?php the_ID(); ?>" <?php post_class(); ?>>
<div class="book-entry-cover">
<?php
if ($cover_url) {
printf('<img src="%s" alt="%s" loading="lazy">', esc_url($cover_url), esc_attr($title));
} else {
printf('<img src="%s" alt="%s">', esc_url(get_stylesheet_directory_uri() . '/assets/images/default-cover.png'), esc_attr__('Book cover', 'bd-bookshelf'));
}
?>
</div>
<div class="book-entry-details">
<?php if ($series) : ?>
<span class="book-series-label">
<a href="<?php echo esc_url(home_url('/series/' . $series['slug'])); ?>">
<?php printf(esc_html__('Part of %s', 'bd-bookshelf'), esc_html($series['name'])); ?>
</a>
</span>
<?php endif; ?>
<h2 class="book-title"><?php the_title(); ?></h2>
<p class="book-author"><?php echo esc_html($author); ?></p>
<?php if (!empty($ratings)) : ?>
<div class="book-ratings">
<?php if (!empty($ratings['amazon'])) : ?>
<span class="rating amazon">Amazon: <?php echo esc_html($ratings['amazon']); ?></span>
<?php endif; ?>
<?php if (!empty($ratings['goodreads'])) : ?>
<span class="rating goodreads">Goodreads: <?php echo esc_html($ratings['goodreads']); ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="book-format-toggle" role="tablist" aria-label="<?php esc_attr_e('Format selection', 'bd-bookshelf'); ?>">
<button class="format-btn active" data-format="ebook" role="tab" aria-selected="true"><?php esc_html_e('Ebook', 'bd-bookshelf'); ?></button>
<button class="format-btn" data-format="paperback" role="tab"><?php esc_html_e('Paperback', 'bd-bookshelf'); ?></button>
<button class="format-btn" data-format="hardback" role="tab"><?php esc_html_e('Hardback', 'bd-bookshelf'); ?></button>
</div>
<div class="book-retailer-links">
<?php
$retailers = array('amazon' => 'Amazon', 'apple' => 'Apple Books', 'barnes' => 'Barnes & Noble', 'kobo' => 'Kobo', 'google' => 'Google Play');
foreach ($retailers as $key => $name) {
$url = get_post_meta($book_id, 'ebook_url_' . $key, true);
if ($url) {
printf(
'<a href="%s" class="retailer-link retailer-%s" target="_blank" rel="noopener noreferrer">%s</a>',
esc_url($url),
esc_attr($key),
esc_html($name)
);
}
}
?>
</div>
<p class="book-blurb"><?php echo wp_kses_post($blurb); ?></p>
</div>
</article>
<?php
endwhile;
wp_reset_postdata();
else :
?>
<p class="no-books"><?php esc_html_e('No books available yet.', 'bd-bookshelf'); ?></p>
<?php endif; ?>
</div>
</div>
</div>
<aside class="page-sidebar">
<?php if (is_active_sidebar('home-sidebar')) : ?>
<div class="home-sidebar">
<?php dynamic_sidebar('home-sidebar'); ?>
</div>
<?php endif; ?>
</aside>
</div>
<?php
get_footer();

View File

@@ -0,0 +1,76 @@
<?php
/**
* Template Name: Series Listing
*
* @package BD_Murphy
*/
get_header();
?>
<div class="page-container with-sidebar">
<div class="page-content">
<div class="series-container">
<header class="page-header">
<h1 class="page-title"><?php esc_html_e('Series', 'bd-bookshelf'); ?></h1>
</header>
<div class="series-list">
<?php
if (have_posts()) :
while (have_posts()) :
the_post();
$series_id = get_the_ID();
$name = esc_html(get_the_title());
$blurb = wp_kses_post(get_the_excerpt() ?: '');
$book_count = 0;
if (class_exists('BD_BookShelfHelper')) {
$book_count = BD_BookShelfHelper::get_series_book_count($series_id);
} else {
$books_in_series = get_post_meta($series_id, '_bdm_series_books', true);
if (is_array($books_in_series)) {
$book_count = count($books_in_series);
}
}
?>
<article class="series-card" <?php post_class(); ?>>
<a href="<?php the_permalink(); ?>">
<?php if (has_post_thumbnail()) : ?>
<?php the_post_thumbnail('series-card'); ?>
<?php endif; ?>
</a>
<h2 class="series-name">
<a href="<?php the_permalink(); ?>"><?php echo $name; ?></a>
</h2>
<p class="series-blurb"><?php echo $blurb; ?></p>
<p class="series-book-count">
<?php
printf(
esc_html(_n('%d book', '%d books', $book_count, 'bd-bookshelf')),
$book_count
);
?>
</p>
</article>
<?php
endwhile;
else :
?>
<p class="no-series"><?php esc_html_e('No series available yet.', 'bd-bookshelf'); ?></p>
<?php endif; ?>
</div>
</div>
</div>
<aside class="page-sidebar">
<?php if (is_active_sidebar('home-sidebar')) : ?>
<div class="home-sidebar">
<?php dynamic_sidebar('home-sidebar'); ?>
</div>
<?php endif; ?>
</aside>
</div>
<?php
get_footer();

Some files were not shown because too many files have changed in this diff Show More