v1.5.0: extend theme with navigation dropdowns, 9 page templates, auth UI, newsletter, contact panel
Major theme extension adding full-featured navigation with dropdown submenus (Tools, My Data, Resources), 9 new page templates (Calculator, Protocol Builder, Tracker, Subject Log, Documentation, Directory, Science Feed, Profile, Sign In/Register), documentation page with sidebar ToC and scroll spy, branded AJAX authentication forms, newsletter signup with admin export, slide-out contact/support panel, Research Modules 6-card grid on front page, and comprehensive footer update. Split functions.php (554→206 lines) into 6 focused inc/ modules: helpers, customizer, auth-handlers, contact-handler, page-setup, newsletter-admin. All new components include dark mode support, CSRF protection, input sanitization, output escaping, and graceful plugin degradation via shortcode_exists(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
137
inc/auth-handlers.php
Normal file
137
inc/auth-handlers.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
/**
|
||||
* Authentication AJAX Handlers
|
||||
*
|
||||
* Handles login and registration via AJAX from the branded auth page.
|
||||
* Both handlers validate CSRF nonces and return JSON responses.
|
||||
*
|
||||
* @see page-auth.php — renders the auth forms
|
||||
* @see assets/js/auth.js — submits forms via XMLHttpRequest
|
||||
* @see functions.php — includes this file
|
||||
*
|
||||
* What: AJAX endpoints for sign-in and registration.
|
||||
* Who calls it: WordPress admin-ajax.php via auth.js.
|
||||
* Dependencies: WordPress core auth functions (wp_signon, wp_create_user).
|
||||
*
|
||||
* @package peptide-starter
|
||||
*/
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle AJAX login.
|
||||
*
|
||||
* Verifies nonce, authenticates via wp_signon(), returns JSON with redirect URL.
|
||||
* Rate limiting: WordPress core handles lockout via wp_login_failed hooks.
|
||||
*
|
||||
* @return void Sends JSON response and exits.
|
||||
*/
|
||||
function peptide_starter_ajax_login() {
|
||||
// Verify CSRF nonce.
|
||||
if ( ! isset( $_POST['ps_login_nonce'] ) ||
|
||||
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['ps_login_nonce'] ) ), 'ps_auth_login' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Security check failed. Please refresh and try again.', 'peptide-starter' ) ) );
|
||||
}
|
||||
|
||||
$email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
|
||||
$password = isset( $_POST['password'] ) ? wp_unslash( $_POST['password'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- password must not be modified.
|
||||
|
||||
if ( empty( $email ) || empty( $password ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Email and password are required.', 'peptide-starter' ) ) );
|
||||
}
|
||||
|
||||
// WordPress authenticates by username or email. Resolve email to username.
|
||||
$user = get_user_by( 'email', $email );
|
||||
|
||||
if ( ! $user ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Invalid email or password.', 'peptide-starter' ) ) );
|
||||
}
|
||||
|
||||
$creds = array(
|
||||
'user_login' => $user->user_login,
|
||||
'user_password' => $password,
|
||||
'remember' => true,
|
||||
);
|
||||
|
||||
$signed_in = wp_signon( $creds, is_ssl() );
|
||||
|
||||
if ( is_wp_error( $signed_in ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Invalid email or password.', 'peptide-starter' ) ) );
|
||||
}
|
||||
|
||||
$redirect = isset( $_POST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_POST['redirect_to'] ) ) : home_url( '/' );
|
||||
// Prevent open redirect attacks — only allow internal URLs.
|
||||
$redirect = wp_validate_redirect( $redirect, home_url( '/' ) );
|
||||
|
||||
wp_send_json_success( array( 'redirect' => $redirect ) );
|
||||
}
|
||||
add_action( 'wp_ajax_nopriv_ps_auth_login', 'peptide_starter_ajax_login' );
|
||||
|
||||
/**
|
||||
* Handle AJAX registration.
|
||||
*
|
||||
* Validates username (5-15 alphanumeric), email, password (min 8 chars).
|
||||
* Creates user via wp_create_user() and auto-logs them in.
|
||||
*
|
||||
* @return void Sends JSON response and exits.
|
||||
*/
|
||||
function peptide_starter_ajax_register() {
|
||||
// Verify CSRF nonce.
|
||||
if ( ! isset( $_POST['ps_register_nonce'] ) ||
|
||||
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['ps_register_nonce'] ) ), 'ps_auth_register' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Security check failed. Please refresh and try again.', 'peptide-starter' ) ) );
|
||||
}
|
||||
|
||||
// Check if registration is enabled in WordPress settings.
|
||||
if ( ! get_option( 'users_can_register' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Registration is currently disabled.', 'peptide-starter' ) ) );
|
||||
}
|
||||
|
||||
$username = isset( $_POST['username'] ) ? sanitize_user( wp_unslash( $_POST['username'] ) ) : '';
|
||||
$email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
|
||||
$password = isset( $_POST['password'] ) ? wp_unslash( $_POST['password'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
|
||||
// Validate username: 5-15 alphanumeric characters.
|
||||
if ( ! preg_match( '/^[a-zA-Z0-9]{5,15}$/', $username ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Username must be 5-15 alphanumeric characters.', 'peptide-starter' ) ) );
|
||||
}
|
||||
|
||||
if ( ! is_email( $email ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Please enter a valid email address.', 'peptide-starter' ) ) );
|
||||
}
|
||||
|
||||
if ( strlen( $password ) < 8 ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Password must be at least 8 characters.', 'peptide-starter' ) ) );
|
||||
}
|
||||
|
||||
if ( username_exists( $username ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'This username is already taken.', 'peptide-starter' ) ) );
|
||||
}
|
||||
|
||||
if ( email_exists( $email ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'An account with this email already exists.', 'peptide-starter' ) ) );
|
||||
}
|
||||
|
||||
$user_id = wp_create_user( $username, $password, $email );
|
||||
|
||||
if ( is_wp_error( $user_id ) ) {
|
||||
wp_send_json_error( array( 'message' => $user_id->get_error_message() ) );
|
||||
}
|
||||
|
||||
// Auto-login after registration.
|
||||
wp_set_current_user( $user_id );
|
||||
wp_set_auth_cookie( $user_id, true, is_ssl() );
|
||||
|
||||
/**
|
||||
* Fires after a new user registers via the frontend auth form.
|
||||
*
|
||||
* @param int $user_id The newly created user ID.
|
||||
*/
|
||||
do_action( 'peptide_starter_user_registered', $user_id );
|
||||
|
||||
wp_send_json_success( array( 'redirect' => home_url( '/profile' ) ) );
|
||||
}
|
||||
add_action( 'wp_ajax_nopriv_ps_auth_register', 'peptide_starter_ajax_register' );
|
||||
94
inc/contact-handler.php
Normal file
94
inc/contact-handler.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
/**
|
||||
* Contact Form AJAX Handler
|
||||
*
|
||||
* Processes the contact/support form from the settings panel.
|
||||
* Sends email via wp_mail() to the site admin.
|
||||
*
|
||||
* @see template-parts/settings-panel.php — renders the contact form
|
||||
* @see assets/js/settings-panel.js — submits form via XMLHttpRequest
|
||||
* @see functions.php — includes this file
|
||||
*
|
||||
* What: AJAX endpoint for the support contact form.
|
||||
* Who calls it: WordPress admin-ajax.php via settings-panel.js.
|
||||
* Dependencies: wp_mail() for email delivery.
|
||||
*
|
||||
* @package peptide-starter
|
||||
*/
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle AJAX contact form submission.
|
||||
*
|
||||
* Validates nonce, sanitizes all inputs, sends email to admin.
|
||||
* Works for both authenticated and unauthenticated users.
|
||||
*
|
||||
* @return void Sends JSON response and exits.
|
||||
*/
|
||||
function peptide_starter_ajax_contact() {
|
||||
// Verify CSRF nonce.
|
||||
if ( ! isset( $_POST['ps_contact_nonce'] ) ||
|
||||
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['ps_contact_nonce'] ) ), 'ps_contact_form' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Security check failed. Please refresh and try again.', 'peptide-starter' ) ) );
|
||||
}
|
||||
|
||||
$name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
|
||||
$email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
|
||||
$topic = isset( $_POST['topic'] ) ? sanitize_text_field( wp_unslash( $_POST['topic'] ) ) : '';
|
||||
$message = isset( $_POST['message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['message'] ) ) : '';
|
||||
|
||||
// Validate required fields.
|
||||
if ( empty( $name ) || empty( $email ) || empty( $topic ) || empty( $message ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'All fields are required.', 'peptide-starter' ) ) );
|
||||
}
|
||||
|
||||
if ( ! is_email( $email ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Please enter a valid email address.', 'peptide-starter' ) ) );
|
||||
}
|
||||
|
||||
// Allowed topics — reject anything unexpected.
|
||||
$allowed_topics = array( 'bug', 'feature', 'data', 'other' );
|
||||
if ( ! in_array( $topic, $allowed_topics, true ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Invalid topic selected.', 'peptide-starter' ) ) );
|
||||
}
|
||||
|
||||
// Build the email.
|
||||
$topic_labels = array(
|
||||
'bug' => __( 'Bug Report', 'peptide-starter' ),
|
||||
'feature' => __( 'Feature Request', 'peptide-starter' ),
|
||||
'data' => __( 'Data Correction', 'peptide-starter' ),
|
||||
'other' => __( 'Other', 'peptide-starter' ),
|
||||
);
|
||||
|
||||
$subject = sprintf(
|
||||
/* translators: 1: topic label, 2: sender name */
|
||||
__( '[Peptide Repo] %1$s from %2$s', 'peptide-starter' ),
|
||||
$topic_labels[ $topic ],
|
||||
$name
|
||||
);
|
||||
|
||||
$body = sprintf( __( 'Name: %s', 'peptide-starter' ), $name ) . "\n";
|
||||
$body .= sprintf( __( 'Email: %s', 'peptide-starter' ), $email ) . "\n";
|
||||
$body .= sprintf( __( 'Topic: %s', 'peptide-starter' ), $topic_labels[ $topic ] ) . "\n\n";
|
||||
$body .= __( 'Message:', 'peptide-starter' ) . "\n";
|
||||
$body .= $message;
|
||||
|
||||
$headers = array(
|
||||
'Reply-To: ' . $name . ' <' . $email . '>',
|
||||
'Content-Type: text/plain; charset=UTF-8',
|
||||
);
|
||||
|
||||
$sent = wp_mail( get_option( 'admin_email' ), $subject, $body, $headers );
|
||||
|
||||
if ( $sent ) {
|
||||
wp_send_json_success( array( 'message' => __( 'Message sent successfully. We will respond within 48 hours.', 'peptide-starter' ) ) );
|
||||
} else {
|
||||
wp_send_json_error( array( 'message' => __( 'Failed to send message. Please try again later.', 'peptide-starter' ) ) );
|
||||
}
|
||||
}
|
||||
add_action( 'wp_ajax_ps_contact_submit', 'peptide_starter_ajax_contact' );
|
||||
add_action( 'wp_ajax_nopriv_ps_contact_submit', 'peptide_starter_ajax_contact' );
|
||||
151
inc/customizer.php
Normal file
151
inc/customizer.php
Normal file
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
/**
|
||||
* Theme Customizer Settings
|
||||
*
|
||||
* Registers all Customizer sections, settings, and controls for
|
||||
* the theme: branding, hero, footer, and dark mode.
|
||||
*
|
||||
* @see functions.php — includes this file
|
||||
* @see front-page.php — reads hero settings
|
||||
* @see footer.php — reads footer copyright
|
||||
*
|
||||
* What: Customizer API registration for theme options.
|
||||
* Who calls it: WordPress customize_register action.
|
||||
* Dependencies: WordPress Customizer API.
|
||||
*
|
||||
* @package peptide-starter
|
||||
*/
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register theme customizer options.
|
||||
*
|
||||
* @param WP_Customize_Manager $wp_customize Customizer manager.
|
||||
* @return void
|
||||
*/
|
||||
function peptide_starter_customize_register( $wp_customize ) {
|
||||
// Branding section.
|
||||
$wp_customize->add_section(
|
||||
'peptide_starter_branding',
|
||||
array(
|
||||
'title' => esc_html__( 'Branding', 'peptide-starter' ),
|
||||
'priority' => 30,
|
||||
)
|
||||
);
|
||||
|
||||
// Hero settings section.
|
||||
$wp_customize->add_section(
|
||||
'peptide_starter_hero',
|
||||
array(
|
||||
'title' => esc_html__( 'Hero Section', 'peptide-starter' ),
|
||||
'priority' => 40,
|
||||
)
|
||||
);
|
||||
|
||||
// Hero Title.
|
||||
$wp_customize->add_setting( 'hero_title', array(
|
||||
'default' => get_bloginfo( 'name' ),
|
||||
'transport' => 'postMessage',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
) );
|
||||
$wp_customize->add_control( 'hero_title', array(
|
||||
'label' => esc_html__( 'Hero Title', 'peptide-starter' ),
|
||||
'section' => 'peptide_starter_hero',
|
||||
'type' => 'text',
|
||||
) );
|
||||
|
||||
// Hero Subtitle.
|
||||
$wp_customize->add_setting( 'hero_subtitle', array(
|
||||
'default' => esc_html__( 'A scientific peptide reference database', 'peptide-starter' ),
|
||||
'transport' => 'postMessage',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
) );
|
||||
$wp_customize->add_control( 'hero_subtitle', array(
|
||||
'label' => esc_html__( 'Hero Subtitle', 'peptide-starter' ),
|
||||
'section' => 'peptide_starter_hero',
|
||||
'type' => 'textarea',
|
||||
) );
|
||||
|
||||
// Hero Search Placeholder.
|
||||
$wp_customize->add_setting( 'hero_search_placeholder', array(
|
||||
'default' => esc_html__( 'Search peptides, sequences, or research...', 'peptide-starter' ),
|
||||
'transport' => 'postMessage',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
) );
|
||||
$wp_customize->add_control( 'hero_search_placeholder', array(
|
||||
'label' => esc_html__( 'Search Placeholder', 'peptide-starter' ),
|
||||
'section' => 'peptide_starter_hero',
|
||||
'type' => 'text',
|
||||
) );
|
||||
|
||||
// Footer section.
|
||||
$wp_customize->add_section(
|
||||
'peptide_starter_footer',
|
||||
array(
|
||||
'title' => esc_html__( 'Footer', 'peptide-starter' ),
|
||||
'priority' => 50,
|
||||
)
|
||||
);
|
||||
|
||||
// Footer Copyright.
|
||||
$wp_customize->add_setting( 'footer_copyright', array(
|
||||
'default' => esc_html__( 'Copyright © 2026 Peptide Repo. All rights reserved.', 'peptide-starter' ),
|
||||
'transport' => 'postMessage',
|
||||
'sanitize_callback' => 'wp_kses_post',
|
||||
) );
|
||||
$wp_customize->add_control( 'footer_copyright', array(
|
||||
'label' => esc_html__( 'Copyright Text', 'peptide-starter' ),
|
||||
'section' => 'peptide_starter_footer',
|
||||
'type' => 'textarea',
|
||||
) );
|
||||
|
||||
// Dark mode section.
|
||||
$wp_customize->add_section(
|
||||
'peptide_starter_theme_mode',
|
||||
array(
|
||||
'title' => esc_html__( 'Theme Mode', 'peptide-starter' ),
|
||||
'priority' => 60,
|
||||
)
|
||||
);
|
||||
|
||||
// Dark mode default.
|
||||
$wp_customize->add_setting( 'dark_mode_default', array(
|
||||
'default' => false,
|
||||
'transport' => 'refresh',
|
||||
'sanitize_callback' => 'peptide_starter_sanitize_checkbox',
|
||||
) );
|
||||
$wp_customize->add_control( 'dark_mode_default', array(
|
||||
'label' => esc_html__( 'Enable Dark Mode by Default', 'peptide-starter' ),
|
||||
'section' => 'peptide_starter_theme_mode',
|
||||
'type' => 'checkbox',
|
||||
) );
|
||||
}
|
||||
add_action( 'customize_register', 'peptide_starter_customize_register' );
|
||||
|
||||
/**
|
||||
* Sanitize checkbox values for the Customizer.
|
||||
*
|
||||
* @param mixed $checked The raw value.
|
||||
* @return bool True if checked, false otherwise.
|
||||
*/
|
||||
function peptide_starter_sanitize_checkbox( $checked ) {
|
||||
return ( isset( $checked ) && true === $checked ) ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add dark mode body class if enabled.
|
||||
*
|
||||
* @param array $classes Existing body classes.
|
||||
* @return array Modified body classes.
|
||||
*/
|
||||
function peptide_starter_dark_mode_body_class( $classes ) {
|
||||
if ( get_theme_mod( 'dark_mode_default', false ) ) {
|
||||
$classes[] = 'dark-mode-default';
|
||||
}
|
||||
return $classes;
|
||||
}
|
||||
add_filter( 'body_class', 'peptide_starter_dark_mode_body_class' );
|
||||
186
inc/helpers.php
Normal file
186
inc/helpers.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
/**
|
||||
* Theme Helper Functions
|
||||
*
|
||||
* Customizer output helpers, nav walker, custom logo, pagination,
|
||||
* and other utility functions shared across the theme.
|
||||
*
|
||||
* @see functions.php — includes this file
|
||||
* @see header.php — uses peptide_starter_the_custom_logo()
|
||||
* @see front-page.php — uses hero getters
|
||||
*
|
||||
* What: Utility functions for template output and theme mods.
|
||||
* Who calls it: Various templates and functions.php.
|
||||
* Dependencies: WordPress Customizer API, Walker_Nav_Menu.
|
||||
*
|
||||
* @package peptide-starter
|
||||
*/
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigation Menu Walker for handling active states.
|
||||
*
|
||||
* Adds an 'active' class to current menu items and their parents
|
||||
* for CSS styling via the .primary-navigation a.active selector.
|
||||
*/
|
||||
class Peptide_Starter_Nav_Walker extends Walker_Nav_Menu {
|
||||
|
||||
/**
|
||||
* Render a single menu item.
|
||||
*
|
||||
* @param string $output Item output.
|
||||
* @param WP_Post $item Menu item data.
|
||||
* @param int $depth Depth of menu item.
|
||||
* @param stdClass $args Menu arguments.
|
||||
* @param int $id Current item ID.
|
||||
* @return void
|
||||
*/
|
||||
public function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) {
|
||||
$indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';
|
||||
$classes = empty( $item->classes ) ? array() : (array) $item->classes;
|
||||
$classes[] = 'menu-item-' . $item->ID;
|
||||
|
||||
if ( in_array( 'current-menu-item', $classes, true ) || in_array( 'current-menu-parent', $classes, true ) ) {
|
||||
$classes[] = 'active';
|
||||
}
|
||||
|
||||
$args = apply_filters( 'nav_menu_item_args', $args, $item, $depth );
|
||||
$class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args, $depth ) );
|
||||
$class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
|
||||
|
||||
$item_id = apply_filters( 'nav_menu_item_id', 'menu-item-' . $item->ID, $item, $args, $depth );
|
||||
$item_id = $item_id ? ' id="' . esc_attr( $item_id ) . '"' : '';
|
||||
|
||||
$output .= $indent . '<li' . $item_id . $class_names . '>';
|
||||
|
||||
$atts = array(
|
||||
'title' => ! empty( $item->attr_title ) ? $item->attr_title : '',
|
||||
'target' => ! empty( $item->target ) ? $item->target : '',
|
||||
'rel' => ! empty( $item->xfn ) ? $item->xfn : '',
|
||||
'href' => ! empty( $item->url ) ? $item->url : '',
|
||||
);
|
||||
|
||||
$atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args, $depth );
|
||||
$attributes = '';
|
||||
|
||||
foreach ( $atts as $attr => $value ) {
|
||||
if ( ! empty( $value ) ) {
|
||||
$value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
|
||||
$attributes .= ' ' . $attr . '="' . $value . '"';
|
||||
}
|
||||
}
|
||||
|
||||
$title = apply_filters( 'the_title', $item->title, $item->ID );
|
||||
|
||||
$link = $args->before;
|
||||
$link .= '<a' . $attributes . '>';
|
||||
$link .= $args->link_before . $title . $args->link_after;
|
||||
$link .= '</a>';
|
||||
$link .= $args->after;
|
||||
|
||||
$output .= $link . "</li>\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display custom logo with fallback to site name.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function peptide_starter_the_custom_logo() {
|
||||
if ( function_exists( 'the_custom_logo' ) ) {
|
||||
the_custom_logo();
|
||||
} else {
|
||||
$site_name = get_bloginfo( 'name' );
|
||||
echo '<a class="site-logo" href="' . esc_url( home_url( '/' ) ) . '" rel="home">' . esc_html( $site_name ) . '</a>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hero title from Customizer.
|
||||
*
|
||||
* @return string Hero title text.
|
||||
*/
|
||||
function peptide_starter_get_hero_title() {
|
||||
return get_theme_mod( 'hero_title', get_bloginfo( 'name' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hero subtitle from Customizer.
|
||||
*
|
||||
* @return string Hero subtitle text.
|
||||
*/
|
||||
function peptide_starter_get_hero_subtitle() {
|
||||
return get_theme_mod( 'hero_subtitle', esc_html__( 'A scientific peptide reference database', 'peptide-starter' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get search placeholder from Customizer.
|
||||
*
|
||||
* @return string Search placeholder text.
|
||||
*/
|
||||
function peptide_starter_get_search_placeholder() {
|
||||
return get_theme_mod( 'hero_search_placeholder', esc_html__( 'Search peptides, sequences, or research...', 'peptide-starter' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get footer copyright text from Customizer.
|
||||
*
|
||||
* @return string Copyright text (may contain safe HTML).
|
||||
*/
|
||||
function peptide_starter_get_footer_copyright() {
|
||||
return get_theme_mod( 'footer_copyright', esc_html__( 'Copyright © 2026 Peptide Repo. All rights reserved.', 'peptide-starter' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if newsletter form should display.
|
||||
*
|
||||
* @return bool Whether to show the newsletter form.
|
||||
*/
|
||||
function peptide_starter_show_newsletter_form() {
|
||||
return apply_filters( 'peptide_starter_show_newsletter', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render pagination links for archive pages.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function peptide_starter_pagination() {
|
||||
global $wp_query;
|
||||
|
||||
if ( $wp_query->max_num_pages <= 1 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$current = max( 1, get_query_var( 'paged' ) );
|
||||
$total = $wp_query->max_num_pages;
|
||||
|
||||
echo '<nav class="ps-pagination">';
|
||||
echo paginate_links(
|
||||
array(
|
||||
'base' => get_pagenum_link( 1 ) . '%_%',
|
||||
'format' => 'page/%#%/',
|
||||
'current' => $current,
|
||||
'total' => $total,
|
||||
'prev_text' => esc_html__( '← Previous', 'peptide-starter' ),
|
||||
'next_text' => esc_html__( 'Next →', 'peptide-starter' ),
|
||||
'type' => 'plain',
|
||||
)
|
||||
);
|
||||
echo '</nav>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific plugin is active by its main file path.
|
||||
*
|
||||
* @param string $plugin Plugin file path relative to plugins directory.
|
||||
* @return bool Whether the plugin is active.
|
||||
*/
|
||||
function peptide_starter_is_plugin_active( $plugin ) {
|
||||
return in_array( $plugin, (array) get_option( 'active_plugins', array() ), true );
|
||||
}
|
||||
129
inc/newsletter-admin.php
Normal file
129
inc/newsletter-admin.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
/**
|
||||
* Newsletter Admin Page
|
||||
*
|
||||
* Simple admin interface to view and export collected newsletter emails.
|
||||
* Emails are stored in wp_options as 'ps_newsletter_emails' until a
|
||||
* proper mailing service (Mailchimp, etc.) is integrated.
|
||||
*
|
||||
* @see functions.php — newsletter signup handler (stores emails)
|
||||
* @see template-parts/newsletter-signup.php — frontend form
|
||||
*
|
||||
* What: Admin page under Tools for viewing/exporting subscriber emails.
|
||||
* Who calls it: WordPress admin_menu action.
|
||||
* Dependencies: None — reads from wp_options.
|
||||
*
|
||||
* @package peptide-starter
|
||||
*/
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the newsletter admin page under Tools.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function peptide_starter_newsletter_admin_menu() {
|
||||
add_management_page(
|
||||
__( 'Newsletter Subscribers', 'peptide-starter' ),
|
||||
__( 'Newsletter', 'peptide-starter' ),
|
||||
'manage_options',
|
||||
'ps-newsletter',
|
||||
'peptide_starter_newsletter_admin_page'
|
||||
);
|
||||
}
|
||||
add_action( 'admin_menu', 'peptide_starter_newsletter_admin_menu' );
|
||||
|
||||
/**
|
||||
* Render the newsletter admin page.
|
||||
*
|
||||
* Displays a simple table of subscriber emails with date added.
|
||||
* Includes a CSV export button.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function peptide_starter_newsletter_admin_page() {
|
||||
$emails = get_option( 'ps_newsletter_emails', array() );
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e( 'Newsletter Subscribers', 'peptide-starter' ); ?></h1>
|
||||
<p>
|
||||
<?php
|
||||
echo esc_html(
|
||||
sprintf(
|
||||
/* translators: %d: number of subscribers */
|
||||
_n( '%d subscriber', '%d subscribers', count( $emails ), 'peptide-starter' ),
|
||||
count( $emails )
|
||||
)
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
|
||||
<?php if ( ! empty( $emails ) ) : ?>
|
||||
<form method="post">
|
||||
<?php wp_nonce_field( 'ps_newsletter_export', 'ps_export_nonce' ); ?>
|
||||
<input type="hidden" name="ps_action" value="export_csv">
|
||||
<?php submit_button( __( 'Export CSV', 'peptide-starter' ), 'secondary', 'export', false ); ?>
|
||||
</form>
|
||||
|
||||
<table class="widefat striped" style="margin-top: 16px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Email', 'peptide-starter' ); ?></th>
|
||||
<th><?php esc_html_e( 'Date Added', 'peptide-starter' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( $emails as $entry ) : ?>
|
||||
<tr>
|
||||
<td><?php echo esc_html( $entry['email'] ); ?></td>
|
||||
<td><?php echo esc_html( $entry['date'] ); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else : ?>
|
||||
<p><?php esc_html_e( 'No subscribers yet.', 'peptide-starter' ); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle CSV export of newsletter subscribers.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function peptide_starter_newsletter_export_csv() {
|
||||
if ( ! isset( $_POST['ps_action'] ) || 'export_csv' !== $_POST['ps_action'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! isset( $_POST['ps_export_nonce'] ) ||
|
||||
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['ps_export_nonce'] ) ), 'ps_newsletter_export' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$emails = get_option( 'ps_newsletter_emails', array() );
|
||||
|
||||
header( 'Content-Type: text/csv; charset=utf-8' );
|
||||
header( 'Content-Disposition: attachment; filename=newsletter-subscribers-' . gmdate( 'Y-m-d' ) . '.csv' );
|
||||
|
||||
$output = fopen( 'php://output', 'w' );
|
||||
fputcsv( $output, array( 'Email', 'Date Added' ) );
|
||||
|
||||
foreach ( $emails as $entry ) {
|
||||
fputcsv( $output, array( $entry['email'], $entry['date'] ) );
|
||||
}
|
||||
|
||||
fclose( $output );
|
||||
exit;
|
||||
}
|
||||
add_action( 'admin_init', 'peptide_starter_newsletter_export_csv' );
|
||||
105
inc/page-setup.php
Normal file
105
inc/page-setup.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
/**
|
||||
* Auto-Create Pages on Theme Activation
|
||||
*
|
||||
* Creates the required WordPress pages (with correct templates assigned)
|
||||
* when the theme is activated, if they don't already exist.
|
||||
*
|
||||
* @see functions.php — includes this file
|
||||
* @see ARCHITECTURE.md — documents the page/template mapping
|
||||
*
|
||||
* What: Theme activation hook that auto-creates pages for all new templates.
|
||||
* Who calls it: WordPress after_switch_theme action.
|
||||
* Dependencies: None — uses WordPress core functions only.
|
||||
*
|
||||
* @package peptide-starter
|
||||
*/
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create required pages on theme activation.
|
||||
*
|
||||
* Checks if each page slug exists; creates it if missing with the
|
||||
* correct page template assigned. Skips existing pages to avoid duplicates.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function peptide_starter_create_pages() {
|
||||
$pages = array(
|
||||
array(
|
||||
'title' => __( 'Calculator', 'peptide-starter' ),
|
||||
'slug' => 'calculator',
|
||||
'template' => 'page-calculator.php',
|
||||
),
|
||||
array(
|
||||
'title' => __( 'Protocol Builder', 'peptide-starter' ),
|
||||
'slug' => 'protocol-builder',
|
||||
'template' => 'page-protocol-builder.php',
|
||||
),
|
||||
array(
|
||||
'title' => __( 'Tracker', 'peptide-starter' ),
|
||||
'slug' => 'tracker',
|
||||
'template' => 'page-tracker.php',
|
||||
),
|
||||
array(
|
||||
'title' => __( 'Subject Log', 'peptide-starter' ),
|
||||
'slug' => 'subject-log',
|
||||
'template' => 'page-subject-log.php',
|
||||
),
|
||||
array(
|
||||
'title' => __( 'Documentation', 'peptide-starter' ),
|
||||
'slug' => 'documentation',
|
||||
'template' => 'page-documentation.php',
|
||||
),
|
||||
array(
|
||||
'title' => __( 'Peptide Directory', 'peptide-starter' ),
|
||||
'slug' => 'peptides',
|
||||
'template' => 'page-directory.php',
|
||||
),
|
||||
array(
|
||||
'title' => __( 'Science Feed', 'peptide-starter' ),
|
||||
'slug' => 'news',
|
||||
'template' => 'page-science-feed.php',
|
||||
),
|
||||
array(
|
||||
'title' => __( 'Profile', 'peptide-starter' ),
|
||||
'slug' => 'profile',
|
||||
'template' => 'page-profile.php',
|
||||
),
|
||||
array(
|
||||
'title' => __( 'Sign In', 'peptide-starter' ),
|
||||
'slug' => 'auth',
|
||||
'template' => 'page-auth.php',
|
||||
),
|
||||
);
|
||||
|
||||
foreach ( $pages as $page_data ) {
|
||||
$existing = get_page_by_path( $page_data['slug'] );
|
||||
|
||||
if ( $existing ) {
|
||||
// Page exists — ensure the template is assigned.
|
||||
update_post_meta( $existing->ID, '_wp_page_template', $page_data['template'] );
|
||||
continue;
|
||||
}
|
||||
|
||||
$page_id = wp_insert_post(
|
||||
array(
|
||||
'post_title' => $page_data['title'],
|
||||
'post_name' => $page_data['slug'],
|
||||
'post_status' => 'publish',
|
||||
'post_type' => 'page',
|
||||
'post_content' => '',
|
||||
'post_author' => 1,
|
||||
)
|
||||
);
|
||||
|
||||
if ( $page_id && ! is_wp_error( $page_id ) ) {
|
||||
update_post_meta( $page_id, '_wp_page_template', $page_data['template'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
add_action( 'after_switch_theme', 'peptide_starter_create_pages' );
|
||||
Reference in New Issue
Block a user