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:
peptiderepo
2026-04-14 08:38:30 +07:00
parent 6c7faa6eed
commit 9da702e746
31 changed files with 4311 additions and 1085 deletions

137
inc/auth-handlers.php Normal file
View 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
View 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
View 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
View 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
View 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
View 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' );