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

5
.gitignore vendored
View File

@@ -20,5 +20,10 @@ composer.lock
# Zip artifacts (built by CI)
*.zip
# Environment & credentials
.env
.env.local
.env.credentials
# AI agent context (local only, not deployed)
.claude/

View File

@@ -2,384 +2,206 @@
## Overview
Peptide Starter (peptide-starter) is a premium, accessible WordPress theme for peptiderepo.com — a scientific peptide reference database. The theme is a classic (non-block) WordPress theme that assembles a modern, responsive frontend by combining a monolithic CSS architecture with vanilla JavaScript. It integrates seamlessly with three companion plugins (Peptide Search AI, Peptide News, PRAutoBlogger) via shortcodes, coordinates dark mode across the ecosystem via CSS custom properties and the DOM `data-theme` attribute, and provides customizable hero and footer sections through the WordPress Customizer.
Peptide Starter (peptide-starter) is a premium, accessible WordPress theme for peptiderepo.com — a scientific peptide reference database. The theme is a classic (non-block) WordPress theme that assembles a modern, responsive frontend by combining a monolithic CSS architecture with vanilla JavaScript. It integrates seamlessly with five companion plugins (Peptide Search AI, Peptide News, PRAutoBlogger, Peptide Tools, Peptide Tracker, Peptide Community) via shortcodes, coordinates dark mode across the ecosystem via CSS custom properties and the DOM `data-theme` attribute, and provides customizable hero, navigation, authentication, and footer sections.
## Directory Structure
```
peptide-starter/
├── style.css # All theme CSS (~44KB). Architecture: reset → tokens → typography →
│ # globals → components → regions → pages → animations → a11y → dark mode → responsive
├── style.css # All theme CSS. Architecture: reset → tokens → typography →
│ # globals → components → regions → pages → animations →
│ # a11y → dark mode → responsive → new feature sections
│
├── functions.php # Theme setup (430 lines)
├── functions.php # Theme setup entry point (~206 lines)
│ # - Constants, feature module loading
│ # - Theme support registration
│ # - Script/style enqueueing with wp_localize_script
│ # - 4x footer widget area registration
│ # - Customizer settings (hero, footer, dark mode)
│ # - Navigation Menu Walker for active states
│ # - Helper functions (get_hero_title, get_footer_copyright, etc.)
│ # - Inline dark mode script in wp_head (for flicker-free initialization)
│ # - Script/style enqueueing (incl conditional)
│ # - Widget areas, newsletter handler, dark mode inline script
│
├── header.php # Global header region (131 lines)
│ # - Site logo/branding
│ # - Primary navigation menu with fallback
│ # - Header icons (search, dark mode toggle, cart, mobile menu)
│ # - Search overlay for Peptide Search AI plugin
│ # - Nav overlay for mobile menu
├── inc/
│ ├── helpers.php # Nav walker, custom logo, hero/footer getters, pagination
│ ├── customizer.php # Customizer sections, settings, sanitize callbacks
│ ├── auth-handlers.php # AJAX login + registration handlers
│ ├── contact-handler.php # AJAX contact form handler (settings panel)
│ ├── page-setup.php # Auto-create pages on theme activation
│ └── newsletter-admin.php # Admin page to view/export subscriber emails
│
├── footer.php # Global footer region (81 lines)
│ # - Newsletter signup form (conditionally rendered)
│ # - 4-column footer widget grid
│ # - Default content fallback per column
│ # - Footer copyright text (from Customizer)
├── header.php # Global header (~190 lines)
│ # - Site logo, primary navigation with dropdowns
│ # - Sign In / User menu (state-aware)
│ # - Settings icon, language placeholder
│ # - Search, dark mode toggle, cart, mobile menu
│ # - Search overlay, fallback nav
│
├── front-page.php # Home page template (86 lines)
│ # - Hero section (title, subtitle, search)
│ # - PRAutoBlogger posts widget [prautoblogger_posts]
│ # - Peptide News feed section [peptide_news]
│ # - Shortcode integration with graceful degradation
├── footer.php # Global footer (~100 lines)
│ # - Newsletter signup (front page via template part)
│ # - 4-column widget grid with updated defaults
│ # - Copyright, disclaimer, language links
│ # - Settings panel include
│
├── page.php # Generic page template (53 lines)
│ # - Page header with title
│ # - Article container
│ # - Edit link display
├── front-page.php # Home page template (~100 lines)
│ # - Hero section (title, subtitle, search, CTAs)
│ # - Research Modules grid (6 cards)
│ # - [prautoblogger_posts] shortcode
│ # - [peptide_news] shortcode
│
├── index.php # Fallback template (27 lines)
│ # - Loop with dynamic post type content dispatch
│ # - Pagination via peptide_starter_pagination()
├── page-calculator.php # Calculator page template → [peptide_tools_calculator]
├── page-protocol-builder.php # Protocol Builder template → [peptide_tools_protocol_builder]
├── page-tracker.php # Tracker template → [peptide_tracker]
├── page-subject-log.php # Subject Log template → [peptide_tracker_subject_log]
├── page-documentation.php # Documentation/SOP page — two-column layout with ToC sidebar
├── page-directory.php # Peptide Directory template → [peptide_directory]
├── page-science-feed.php # Science Feed template → [peptide_news] + newsletter
├── page-profile.php # User Profile template → [peptide_community_profile]
├── page-auth.php # Sign In / Register — branded auth forms with AJAX
│
├── page.php # Generic page template
├── index.php # Fallback template
├── single-peptide.php # Peptide detail page (CPT)
│ # - Renders single peptide post
│
├── archive-peptide.php # Peptide archive (CPT grid)
│ # - Displays peptide CPT items in grid layout
│
├── 404.php # 404 error page
│ # - Default 404 handling
│
├── searchform.php # Search form template
│ # - Standard WordPress search form
│
├── template-parts/
│ ├── content.php # Default post/page loop item (19 lines)
│ # - Title, date, author, excerpt, read more link
│
│ ├── content.php # Default post/page loop item
│ ├── content-peptide.php # Peptide CPT card template
│ # - Peptide-specific card styling with .ps-card
│ ├── content-none.php # Empty state template
│ ├── module-cards.php # Research Modules 6-card grid (front page)
│ ├── newsletter-signup.php # Reusable newsletter signup section
│ └── settings-panel.php # Slide-out support/contact panel
│
│ └── content-none.php # Empty state template
│ # - Message when no posts found
│
├── assets/
│ └── js/
├── assets/js/
│ ├── navigation.js # Mobile menu toggle (85 lines)
│ # - Mobile hamburger menu state management
│ # - Nav overlay click handling
│ # - Escape key, resize, link click closing
│ # - aria-expanded attribute updates
│
│ └── theme.js # Dark mode + utilities (211 lines)
│ # - Dark mode toggle & localStorage persistence
│ # - System preference detection (prefers-color-scheme)
│ # - Custom 'themechange' event dispatch to plugins
│ # - Search overlay open/close functionality
│ # - Icon state toggling (sun/moon)
│ # - window.peptideStarterTheme API for plugin access
│ # - Smooth scroll + focus-visible polyfill
│ ├── theme.js # Dark mode + search overlay + utilities (210 lines)
│ ├── documentation.js # ToC generation + scroll spy for docs page (145 lines)
│ ├── auth.js # Auth form toggle, validation, AJAX (192 lines)
│ └── settings-panel.js # Settings panel open/close + contact form (163 lines)
│
├── languages/ # Translation files (.pot, .po, .mo)
│ # - Text domain: peptide-starter
│
├── tests/
│ ├── bootstrap.php # PHPUnit bootstrap — loads WP test framework + theme
│ ├── test-functions.php # Tests for helper functions (hero, footer, newsletter, sanitize)
│ └── test-theme-setup.php # Tests for theme supports, menus, widgets, scripts
│
├── .phpcs.xml.dist # PHPCS config — WordPress-Core standard
├── phpunit.xml # PHPUnit config — points to tests/ directory
├── composer.json # Dev dependencies (PHPUnit, PHPCS, WPCS)
├── README.md # User-facing documentation
├── CLAUDE.md # Developer context notes
├── tests/ # PHPUnit tests
├── .phpcs.xml.dist # PHPCS config
├── phpunit.xml # PHPUnit config
├── composer.json # Dev dependencies
├── README.md # User-facing docs
├── CLAUDE.md # Developer context
├── CHANGELOG.md # Version history
└── screenshot.png # WordPress theme preview image
└── screenshot.png # Theme preview image
```
## Navigation Structure
```
Home | Tools ▾ | My Data ▾ | Resources ▾ | [Sign In / Username ▾] ⚙ 🌐 🔍 🌙
Tools dropdown:
├── Calculator → /calculator
├── Protocol Builder → /protocol-builder
└── Tracker → /tracker
My Data dropdown:
├── Peptides → /peptides
└── Subject Log → /subject-log
Resources dropdown:
├── Documentation → /documentation
└── Science Feed → /news
User menu (logged in):
├── My Profile → /profile
├── Tracker → /tracker
├── Subject Log → /subject-log
└── Sign Out
```
## Page Template Map
| Page Title | Slug | Template File | Plugin Shortcode |
|------------|------|---------------|-----------------|
| Calculator | calculator | page-calculator.php | [peptide_tools_calculator] |
| Protocol Builder | protocol-builder | page-protocol-builder.php | [peptide_tools_protocol_builder] |
| Tracker | tracker | page-tracker.php | [peptide_tracker] |
| Subject Log | subject-log | page-subject-log.php | [peptide_tracker_subject_log] |
| Documentation | documentation | page-documentation.php | (WordPress content + JS ToC) |
| Peptide Directory | peptides | page-directory.php | [peptide_directory] |
| Science Feed | news | page-science-feed.php | [peptide_news] |
| Profile | profile | page-profile.php | [peptide_community_profile] |
| Sign In | auth | page-auth.php | (Custom auth forms) |
Pages are auto-created on theme activation via `inc/page-setup.php`.
## Data Flow & Architecture Diagrams
### Front Page Assembly (Plugin Integration)
### Front Page Assembly
```
front-page.php
├─→ Hero Section (title, subtitle, search, CTAs)
│ └─→ [peptide_search] shortcode (Peptide Search AI plugin)
│
├─→ Hero Section (title, subtitle, search)
│ │
│ └─→ [peptide_search] shortcode
│ └─→ Peptide Search AI plugin (if active)
├─→ Research Modules Grid (template-parts/module-cards.php)
│ └─→ 6 cards linking to tool/section pages
│ └─→ Checks shortcode_exists() for graceful degradation
│
├─→ Hero CTAs (Browse Peptides, Learn More)
├─→ [prautoblogger_posts] shortcode (PRAutoBlogger plugin)
│
├─→ [prautoblogger_posts] shortcode
│ └─→ PRAutoBlogger plugin
│ └─→ Renders 6 auto-generated research articles
└─→ [peptide_news] shortcode (Peptide News plugin)
```
### Authentication Flow
```
page-auth.php
├─→ Sign In form → auth.js AJAX → inc/auth-handlers.php
│ └─→ wp_signon() → redirect to original page
│
└─→ [peptide_news] shortcode
└─→ Peptide News plugin
└─→ Renders news feed grid
└─→ Register form → auth.js AJAX → inc/auth-handlers.php
└─→ wp_create_user() → auto-login → redirect to /profile
```
### Dark Mode Coordination
```
functions.php (wp_head)
│
└─→ Inline script runs first (before CSS loads)
└─→ Sets html[data-theme] from:
1. localStorage (user's choice)
2. prefers-color-scheme (system)
3. dark_mode_default (Customizer setting)
└─→ Prevents flash of wrong color on page load
│
├─→ style.css reads data-theme
│ └─→ @media selector: html[data-theme="dark"]
│
└─→ theme.js detects theme changes
└─→ Dispatches 'themechange' event
└─→ Updates localStorage
└─→ Updates icon visibility
└─→ Plugins listen & update own styles
```
### Template Hierarchy & Routing
```
WordPress Request
│
├─→ is_front_page() ? front-page.php
│ └─→ Hero + [prautoblogger_posts] + [peptide_news]
│
├─→ is_singular('peptide') ? single-peptide.php
│ └─→ get_template_part('content-peptide')
│
├─→ is_archive() ? archive-peptide.php
│ └─→ Grid of peptide cards
│
├─→ is_page() ? page.php
│ └─→ get_template_part('content')
│
├─→ is_search() || is_category() || is_tag() ? index.php
│ └─→ Loop: get_template_part('content', get_post_type())
│ └─→ Dispatches to content.php or content-{type}.php
│
├─→ is_404() ? 404.php
│
└─→ Default ? index.php
└─→ Fallback loop template
```
### Script Loading & Dark Mode API
```
wp_enqueue_scripts (functions.php)
│
├─→ peptide-starter-style
│ └─→ style.css (PEPTIDE_STARTER_VERSION)
│
├─→ peptide-starter-navigation
│ └─→ assets/js/navigation.js (in footer, async)
│
└─→ peptide-starter-theme
├─→ assets/js/theme.js (in footer, async)
└─→ wp_localize_script('peptideStarterData')
├─→ siteUrl: home_url('/')
└─→ isDarkMode: get_theme_mod('dark_mode_default')
```
### Plugin Dark Mode Listening
Plugins can coordinate with the theme via three mechanisms:
```javascript
// 1. Custom event (recommended)
document.addEventListener('themechange', (e) => {
console.log('Theme changed to:', e.detail.theme); // 'dark' | 'light'
});
// 2. DOM attribute mutation
const observer = new MutationObserver(() => {
const theme = document.documentElement.getAttribute('data-theme');
});
observer.observe(document.documentElement, { attributes: true });
// 3. CSS custom properties (theme.js updates these)
const color = getComputedStyle(document.documentElement).getPropertyValue('--text-primary');
functions.php (wp_head inline script)
└─→ Sets html[data-theme] from localStorage → system pref → customizer default
├─→ style.css reads data-theme for color overrides
└─→ theme.js dispatches 'themechange' event to plugins
```
## External Integrations
### Plugin Integrations (via Shortcodes)
1. **Peptide Search AI** (`[peptide_search]`)
- Location: Hero section (header.php, front-page.php)
- Fallback: Standard WordPress search form
- Dark mode: Listens to `themechange` event
- Namespace: `.psa-*`
1. **Peptide Search AI** (`[peptide_search]`) — Hero + search overlay. Namespace: `.psa-*`
2. **Peptide News** (`[peptide_news]`) — Front page + science feed. Namespace: `.pn-*`
3. **PRAutoBlogger** (`[prautoblogger_posts]`) — Front page articles. Namespace: `.prab-*`
4. **Peptide Tools** (`[peptide_tools_calculator]`, `[peptide_tools_protocol_builder]`) — Tool pages
5. **Peptide Tracker** (`[peptide_tracker]`, `[peptide_tracker_subject_log]`) — Tracker pages
6. **Peptide Community** (`[peptide_community_profile]`, `[peptide_directory]`) — Profile + directory
2. **Peptide News** (`[peptide_news]`)
- Location: Front page (front-page.php)
- Renders: News feed grid with article cards
- Dark mode: Listens to `themechange` event
- Namespace: `.pn-*`
- Override: `functions.php` line 80-90 removes `-webkit-line-clamp` on excerpts
3. **PRAutoBlogger** (`[prautoblogger_posts]`)
- Location: Front page (front-page.php)
- Renders: 6 auto-generated research articles section
- Dark mode: Listens to `themechange` event
- Namespace: `.prab-*`
All plugins are checked with `shortcode_exists()` for graceful degradation.
### WooCommerce Integration
- Cart icon in header (header.php line 68)
- Conditionally rendered if `class_exists('WooCommerce')`
- Minimal theme styling; WooCommerce handles its own UI
- Icon: SVG shopping cart
### Google Fonts Integration
- Font: **Inter** (from Google Fonts)
- Loaded via CSS `@import url('...')`
- Fallback: System fonts (`-apple-system, BlinkMacSystemFont, etc.`)
- Display: `display=swap` for optimal loading
### REST API & Customizer
- Theme mods saved to `wp_options` table
- Customizer sections: Branding, Hero Section, Footer, Theme Mode
- All settings use `'transport' => 'postMessage'` for instant preview (except dark_mode_default which uses refresh)
All plugins checked with `shortcode_exists()` for graceful degradation.
## Key Architectural Decisions
### 1. Classic Theme (Not FSE/Block Theme)
1. **Classic Theme (Not FSE)** — Stability, control, plugin compatibility
2. **Monolithic CSS** — No build step, CSS custom properties for abstraction
3. **Vanilla JavaScript** — No bundler, direct browser ES6+, `node --check` validation
4. **Shortcode Integration** — Decouples plugins from theme via `shortcode_exists()`
5. **CSS Custom Properties for Dark Mode** — Single source of truth for colors
6. **No `!important` Policy** — Explicit specificity over cascade tricks
7. **Inline Dark Mode Script** — Prevents flash of wrong theme on load
8. **Split functions.php into inc/** — Keeps all files under 300 lines
**Why:** Peptide Repo predates WordPress Full Site Editing. Classic themes provide:
- Simpler, more stable template hierarchy for developers
- Full control over HTML structure without block markup
- Easier plugin compatibility without block interdependencies
- Proven stability for production scientific database
## Security
### 2. Monolithic CSS (Not SASS/LESS)
**Why:**
- No build step = instant deploy without compilation
- Single 44KB file loads fast (gzips to ~7KB)
- CSS custom properties (variables) provide all needed abstraction
- Theme can be edited directly on remote server if needed
- Reduces deployment complexity on Hostinger shared hosting
### 3. Vanilla JavaScript (No Bundler)
**Why:**
- Simple, maintainable code without build toolchain
- Direct browser compatibility (ES6+)
- Minimal overhead; only 296 lines total across 2 files
- Each script can be tested by running `node --check` syntax validation
- No dependencies = no supply chain risk
### 4. Shortcode Integration (Not Direct Function Calls)
**Why:**
- Plugins are independent; shortcodes decouple content from theme
- Shortcodes can be disabled/removed without theme breaking
- `shortcode_exists()` provides graceful degradation
- Theme doesn't need to know plugin implementation details
- Content (front-page.php) stays separate from plugin business logic
### 5. CSS Custom Properties for Dark Mode & Theming
**Why:**
- Single source of truth for all colors (defined in `:root`)
- Plugins can read theme colors via `getComputedStyle()`
- Easy to switch between light/dark without duplicate selectors
- No need for SASS mixins or preprocessor
- Future-proof: standard CSS, works in all modern browsers
### 6. No `!important` Policy (Global Scale)
**Why:**
- `!important` breaks plugin CSS specificity
- Forces explicit specificity through selector weight instead
- Theme respects plugin namespaces (`.pn-*`, `.psa-*`, `.prab-*`)
- Only exception: line 85 (necessary override of Peptide News line-clamp)
- Maintains CSS maintainability and plugin harmony
### 7. Inline Dark Mode Script in `wp_head`
**Why:**
- Runs *before* CSS loads, preventing white flash in dark mode
- Reads localStorage synchronously (no async needed here)
- Sets `data-theme` attribute before first paint
- Fallback chain: localStorage → system preference → customizer default
- Performance impact: minimal (~1ms execution)
### 8. Navigation Walker with Active State Detection
**Why:**
- Custom Walker class (line 243-300) detects current-menu-item
- Adds `.active` class for CSS styling (matches WordPress conventions)
- Enables dynamic nav styling without JS
- Integrates with WordPress menus natively
- Fallback function for sites with no primary menu
### 9. Helper Functions for Customizer Output
**Why:**
- `peptide_starter_get_*()` functions (lines 317-340) centralize theme mod retrieval
- Consistent defaults across templates
- Easy to update copy site-wide from one place
- Separates data layer from presentation
## Security & Performance Considerations
- **Escaping:** All output uses `esc_html()`, `esc_url()`, `esc_attr()` appropriately
- **Nonces:** Newsletter form uses `wp_nonce_field()` with CSRF verification in handler
- **Sanitize callbacks:** All Customizer settings have explicit `sanitize_callback` (text, kses, checkbox)
- **Form handling:** Newsletter signup routes through `admin_post` action with nonce + email validation
- **Permissions:** Check `is_active_sidebar()` before rendering widgets
- **Performance:** Script/style versions use `PEPTIDE_STARTER_VERSION` constant for cache busting
- **Accessibility:** Skip-to-main-content link (header.php line 20), WCAG AA color contrast, focus indicators
## Testing & CI/CD
- **CI pipeline** (`.github/workflows/deploy.yml`):
1. PHP lint (`php -l`) on all PHP files
2. PHPCS with WordPress-Core standard
3. JS syntax check (`node --check`) on all JS files
4. Deploy via rsync (only if validation passes)
5. Post-deploy health check (HTTP 200)
- **Unit tests** (`tests/`): PHPUnit with WordPress test framework
- `test-functions.php` — helpers, sanitizers, filters
- `test-theme-setup.php` — theme supports, menus, widgets, scripts
- **rsync safety**: Dev-only files excluded from deploy (tests/, vendor/, composer.json, phpunit.xml, .phpcs.xml.dist)
## Browser Support
- Chrome/Edge (latest 2 versions)
- Firefox (latest 2 versions)
- Safari (latest 2 versions)
- iOS Safari 12+
- Chrome Android
- CSRF nonces on all forms (auth, newsletter, contact)
- All input sanitized at boundary (`sanitize_text_field`, `sanitize_email`, `sanitize_textarea_field`)
- All output escaped (`esc_html`, `esc_attr`, `esc_url`, `wp_kses_post`)
- Login redirect validated with `wp_validate_redirect()` to prevent open redirects
- Auth form uses AJAX to avoid exposing credentials in URL
- Contact form topics restricted to allowlist
## Version
**Current:** v1.3.1 (defined in `style.css` header line 7 and `functions.php` line 14 constant)
**Current:** v1.5.0
When releasing a new version, update both locations.
## Related Documentation
- **CONVENTIONS.md** — How to add templates, components, plugin integrations
- **CLAUDE.md** — Developer context and quick reference
- **README.md** — User-facing feature list and installation
- **CHANGELOG.md** — Release history
When releasing a new version, update `style.css` header (line 7) and `PEPTIDE_STARTER_VERSION` in `functions.php`.

79
CLAUDE.md Normal file
View File

@@ -0,0 +1,79 @@
# Peptide Starter Theme
WordPress theme for peptiderepo.com — a scientific peptide reference database.
## Repository
- **GitHub**: `peptiderepo/peptide-starter-theme` (private)
- **Branch**: `main` is the production branch
- **Live site**: https://peptiderepo.com
- **Hosting**: Hostinger shared hosting
## Deploy
Push to `main` triggers GitHub Actions → rsync via SSH to Hostinger.
No CI lint step — the deploy workflow (`deploy.yml`) runs rsync directly.
LiteSpeed cache is purged automatically after deploy.
Theme path on server: `~/domains/peptiderepo.com/public_html/wp-content/themes/peptide-starter/`
## Architecture
This is a classic WordPress theme (no block/FSE). All styling is in `style.css` (no build step, no preprocessor). JavaScript is vanilla — no bundler.
### Key files
| File | Purpose |
|------|---------|
| `style.css` | All theme CSS (~44KB). Organized: reset → tokens → typography → globals → components → regions → pages → animations → a11y → dark mode → responsive |
| `functions.php` | Theme setup, script enqueueing, Customizer registration, nav walker, helper functions |
| `front-page.php` | Home page: hero section → PRAutoBlogger widget → Peptide News feed |
| `header.php` | Global header with nav, dark mode toggle, mobile menu |
| `footer.php` | 4-column widget footer with copyright |
| `archive-peptide.php` | Peptide CPT archive grid |
| `single-peptide.php` | Single peptide detail page |
| `assets/js/navigation.js` | Mobile menu and nav functionality |
| `assets/js/theme.js` | Dark mode toggle, theme utilities |
### Template parts
- `template-parts/content.php` — Post/page content loop
- `template-parts/content-peptide.php` — Peptide card in archive
- `template-parts/content-none.php` — Empty state
## Coding Conventions
- **PHP**: WordPress coding standards — tabs for indentation, Yoda conditions, braces on same line
- **CSS**: No preprocessor. Use CSS custom properties (defined in `:root`). All theme classes prefixed `.ps-`
- **JS**: Vanilla ES6+, no build step. Files in `assets/js/` must pass `node --check`
- **No `!important`** on global elements — prevents plugin conflicts
- Plugin CSS namespaces are untouched (`.pn-*` for Peptide News, `.psa-*` for Peptide Search AI, `.prab-*` for PRAutoBlogger)
## Design System
- **Font**: Inter (Google Fonts) with system fallback
- **Scale**: 1.125x modular, 8px grid base
- **Primary color**: `#0066CC` (light) / `#3B82F6` (dark)
- **Breakpoints**: 480px, 768px, 1024px, 1440px (mobile-first)
- **Dark mode**: CSS custom properties swap via `html[data-theme="dark"]` or `prefers-color-scheme: dark`
## Plugin Integration
The theme integrates with three companion plugins via shortcodes, all wrapped in `shortcode_exists()` for graceful degradation:
1. **Peptide Search AI** (`[peptide_search]`) — AI search in hero section
2. **Peptide News** (`[peptide_news]`) — News feed on front page
3. **PRAutoBlogger** (`[prautoblogger_posts]`) — Auto-generated research articles on front page
## Customizer Options
Editable via Appearance → Customize:
- Hero title, subtitle, search placeholder
- Footer copyright text
- Dark mode default on/off
## Version
Current: **v1.3.1** (defined in `style.css` header and `PEPTIDE_STARTER_VERSION` constant in `functions.php`)
When bumping version, update both locations.

600
CONVENTIONS.md Normal file
View File

@@ -0,0 +1,600 @@
# Peptide Starter Theme - Conventions & Development Guide
## Naming Conventions
### PHP Functions
All custom PHP functions are prefixed with `peptide_starter_`:
```php
// Good
function peptide_starter_get_hero_title() { }
function peptide_starter_setup() { }
function peptide_starter_widgets_init() { }
function peptide_starter_pagination() { }
// Bad
function get_hero_title() { }
function setup_theme() { }
```
**Pattern:** `peptide_starter_{verb}_{noun}` or `peptide_starter_get_{option}`
**Common function types:**
- `peptide_starter_get_*()` — Retrieve theme mod or option
- `peptide_starter_{noun}_init()` — Initialization functions
- `peptide_starter_is_*()` — Boolean checks
- `peptide_starter_render_*()` — Output HTML
### CSS Classes
All theme CSS classes are prefixed with `.ps-`:
```css
/* Components */
.ps-btn /* Button component */
.ps-btn-primary /* Button variant */
.ps-btn-sm /* Button size */
.ps-card /* Card component */
.ps-badge /* Badge component */
.ps-alert-info /* Alert variant */
/* Utilities */
.ps-container /* Max-width wrapper */
.ps-pagination /* Pagination nav */
.ps-search-form /* Search form */
.ps-search-input /* Search input */
/* Regions */
.site-header /* No prefix; global region */
.site-footer /* No prefix; global region */
.site-main /* No prefix; global region */
.hero-section /* Hero region specific */
```
**Pattern:** `.ps-{component}` for components, `.ps-{component}-{variant}` for variants
**Reserved prefixes (do not use):**
- `.pn-*` — Peptide News plugin
- `.psa-*` — Peptide Search AI plugin
- `.prab-*` — PRAutoBlogger plugin
### CSS Variables (Custom Properties)
All design tokens are defined in `style.css :root`:
```css
/* Colors */
--text-primary /* Main text color */
--text-secondary /* Secondary text color */
--bg-primary /* Main background */
--bg-secondary /* Secondary background */
/* Spacing (8px grid base) */
--spacing-xs: 4px /* 0.5 × base */
--spacing-sm: 8px /* 1 × base */
--spacing-md: 16px /* 2 × base */
--spacing-lg: 24px /* 3 × base */
--spacing-xl: 32px /* 4 × base */
/* Typography */
--font-sans /* Primary font family */
--font-mono /* Monospace font */
--text-h1 /* H1 size */
--text-h1-weight /* H1 weight */
```
**Usage:**
```css
.ps-card {
padding: var(--spacing-md);
color: var(--text-primary);
}
```
### Template Part Naming
Template parts in `template-parts/` follow WordPress convention:
```
template-parts/content.php /* Default loop item */
template-parts/content-{post-type}.php /* CPT-specific */
template-parts/content-none.php /* Empty state */
template-parts/{section}-{name}.php /* Specific parts (if added) */
```
**Calling template parts:**
```php
get_template_part( 'template-parts/content', get_post_type() );
get_template_part( 'template-parts/content', 'none' );
```
### Hook Names
Custom hooks use the `peptide_starter_` prefix:
```php
// Filters
apply_filters( 'peptide_starter_show_newsletter', true );
// Actions (if any added in future)
do_action( 'peptide_starter_hero_before' );
```
### File Organization
- **Root:** Theme core files (functions.php, header.php, footer.php, etc.)
- **assets/js/:** Client-side scripts only; no bundling
- **assets/css/:** (None; all CSS in style.css)
- **languages/:** Translation files
- **template-parts/:** Reusable template parts
## Step-by-Step Guides
### How to Add a New Template
**Scenario:** Create a custom landing page template
1. Create file: `landing-{name}.php`
```php
<?php
/**
* Landing Page: {Description}
* Template Name: {Display Name}
*
* @package peptide-starter
*/
get_header();
?>
<main id="main" class="site-main">
<!-- Your content here -->
</main>
<?php get_footer();
```
2. Add template header comment:
```php
/**
* Template Name: My Custom Landing Page
* Template Post Type: page
*/
```
3. The template will appear in WordPress Page > Template dropdown
4. Test the template:
- Create/edit a page and select the template
- Verify hero, dark mode toggle, footer all appear correctly
**Best practices:**
- Always include `get_header()` and `get_footer()`
- Wrap main content in `<main id="main" class="site-main">`
- Use `.ps-container` for consistent max-width
- Use existing component classes (`.ps-card`, `.ps-btn`, etc.)
- Keep templates under 150 lines; extract complex parts to `template-parts/`
### How to Add a New Plugin Integration
**Scenario:** Integrate a new plugin (e.g., "Research Database") via shortcode
1. **In front-page.php**, add a conditional shortcode block:
```php
<?php if ( shortcode_exists( 'research_db' ) ) { ?>
<section class="research-db-section">
<div class="ps-container">
<h2><?php esc_html_e( 'Research Database', 'peptide-starter' ); ?></h2>
<?php echo do_shortcode( '[research_db limit="10"]' ); ?>
</div>
</section>
<?php } ?>
```
2. **In functions.php**, optionally add inline overrides if plugin CSS needs adjustment:
```php
function peptide_starter_plugin_overrides() {
wp_add_inline_style( 'peptide-starter-style', '
/* Override plugin styles here */
.rdb-card { /* example */ }
' );
}
add_action( 'wp_enqueue_scripts', 'peptide_starter_plugin_overrides', 99 );
```
3. **Document the integration** in CLAUDE.md:
- Plugin namespace (e.g., `.rdb-*`)
- Shortcode name and parameters
- Expected styling classes
4. **If plugin needs dark mode coordination:**
- Ask plugin author to listen to `themechange` event (see ARCHITECTURE.md)
- Or have plugin read `html[data-theme]` attribute
- Provide plugin with CSS custom properties: `getComputedStyle(document.documentElement).getPropertyValue('--text-primary')`
5. **Test:**
- Activate plugin and visit front page
- Verify shortcode renders
- Toggle dark mode; verify plugin responds
- Deactivate plugin; verify page still renders (fallback gracefully)
**Important:** Always wrap shortcodes in `shortcode_exists()` checks so theme doesn't break if plugin is missing.
### How to Add a New CSS Component
**Scenario:** Create a new "pill" button style (small, rounded, inline)
1. **In style.css**, find the Components section (around line 500)
2. **Add new component class:**
```css
/* Pill buttons */
.ps-btn-pill {
display: inline-block;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: 9999px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.ps-btn-pill.ps-btn-primary {
background-color: var(--color-primary);
color: white;
}
.ps-btn-pill.ps-btn-secondary {
background-color: transparent;
border: 1px solid var(--color-primary);
color: var(--color-primary);
}
```
3. **Add dark mode support** (in Dark Mode section, around line 1200):
```css
html[data-theme="dark"] .ps-btn-pill.ps-btn-secondary {
border-color: var(--color-primary-dark);
color: var(--color-primary-dark);
}
```
4. **Add responsive sizes if needed** (in Responsive section, around line 1400):
```css
@media (max-width: 767px) {
.ps-btn-pill {
padding: var(--spacing-xs) var(--spacing-xs);
}
}
```
5. **Usage in templates:**
```html
<span class="ps-btn ps-btn-pill ps-btn-primary">Beta</span>
```
6. **Test:**
- Light mode: verify colors, sizing, alignment
- Dark mode: toggle and verify contrast
- Mobile (480px): verify responsive adjustments
- Keyboard: focus state should be visible
**Best practices:**
- Use CSS variables (`var(--spacing-*)`, `var(--color-*)`) not hardcoded values
- Avoid `!important` unless absolutely necessary
- Include dark mode support immediately (no `.ps-*` is light-only)
- Group related rules with comments
- Keep component definitions self-contained (<100 lines)
### How to Add a New Customizer Setting
**Scenario:** Add a new "Primary Accent Color" customizer option
1. **In functions.php** (around line 143), add a new section:
```php
$wp_customize->add_section( 'peptide_starter_colors', array(
'title' => esc_html__( 'Colors', 'peptide-starter' ),
'priority' => 45,
) );
```
2. **Add a setting:**
```php
$wp_customize->add_setting( 'primary_accent_color', array(
'default' => '#0066CC',
'transport' => 'postMessage',
'sanitize_callback' => 'sanitize_hex_color',
) );
```
3. **Add a control:**
```php
$wp_customize->add_control( new WP_Customize_Color_Control(
$wp_customize,
'primary_accent_color',
array(
'label' => esc_html__( 'Primary Accent Color', 'peptide-starter' ),
'section' => 'peptide_starter_colors',
'settings' => 'primary_accent_color',
)
) );
```
4. **Create a getter function:**
```php
function peptide_starter_get_primary_accent_color() {
return get_theme_mod( 'primary_accent_color', '#0066CC' );
}
```
5. **Output in front-end** (e.g., in footer.php):
```php
<style>
:root {
--color-accent: <?php echo esc_attr( peptide_starter_get_primary_accent_color() ); ?>;
}
</style>
```
Or better, use inline style in `wp_head`:
```php
function peptide_starter_custom_colors() {
$accent = peptide_starter_get_primary_accent_color();
?>
<style>
:root {
--color-accent: <?php echo esc_attr( $accent ); ?>;
}
</style>
<?php
}
add_action( 'wp_head', 'peptide_starter_custom_colors' );
```
6. **Reference in CSS:**
```css
.ps-btn-accent {
background-color: var(--color-accent);
}
```
7. **Test:**
- Appearance → Customize → Colors → Primary Accent Color
- Change color and verify live preview
- Save and reload page
**Transport options:**
- `'transport' => 'postMessage'` — Instant preview (needs JS listener)
- `'transport' => 'refresh'` — Full page refresh
For color/text changes, use `postMessage` with a JS listener in theme.js:
```javascript
wp_customize.bind( 'setting:primary_accent_color', function( setting ) {
setting.bind( function( newval ) {
document.documentElement.style.setProperty('--color-accent', newval);
});
});
```
## Error Handling Patterns
### Theme Doesn't Break If Plugin Missing
**Pattern: Graceful Degradation**
```php
// Good — shortcode_exists check
if ( shortcode_exists( 'peptide_search' ) ) {
echo do_shortcode( '[peptide_search]' );
} else {
get_search_form(); // Fallback to WordPress default
}
// Bad — assumes plugin exists
echo do_shortcode( '[peptide_search]' ); // Renders [peptide_search] as text if missing
```
### Menu Fallback
**In header.php:**
```php
wp_nav_menu( array(
'theme_location' => 'primary',
'fallback_cb' => 'peptide_starter_primary_menu_fallback', // Function defined in header
'walker' => new Peptide_Starter_Nav_Walker(),
) );
function peptide_starter_primary_menu_fallback() {
// Renders Home, About, Peptides links if no menu assigned
}
```
### Widget Area Check
**In footer.php:**
```php
if ( is_active_sidebar( 'footer-1' ) ) {
dynamic_sidebar( 'footer-1' );
} else {
// Render default content
echo '<h3>' . esc_html__( 'About', 'peptide-starter' ) . '</h3>';
}
```
### Class Existence Check
**For WooCommerce:**
```php
if ( class_exists( 'WooCommerce' ) ) {
// Render cart icon
}
```
**For post types:**
```php
if ( post_type_exists( 'peptide' ) ) {
echo '<a href="' . get_post_type_archive_link( 'peptide' ) . '">' . __( 'Peptides' ) . '</a>';
}
```
### Input Validation
All user-facing output must be escaped:
```php
// Good
echo esc_html( get_the_title() );
echo esc_url( home_url( '/' ) );
echo esc_attr( $html_attribute_value );
// Bad
echo get_the_title();
echo home_url( '/' );
```
## Dark Mode Implementation Guide
### How Dark Mode Works in Peptide Starter
1. **Inline script in `wp_head`** (functions.php, line 406-422)
- Runs immediately before CSS loads
- Prevents white flash by setting `html[data-theme]` early
- Checks in order: localStorage → system pref → customizer default
2. **CSS selectors** (style.css, line ~1200 "Dark Mode")
- Light mode: default colors in `:root`
- Dark mode: `html[data-theme="dark"]` selector overrides
3. **JavaScript listener** (theme.js, line 47-63)
- Responds to dark mode toggle button
- Updates localStorage
- Dispatches `themechange` event to plugins
- Updates sun/moon icon visibility
### Adding Dark Mode to a New Component
**Example: New Alert Style**
1. **Define light mode in CSS:**
```css
.ps-alert-custom {
padding: var(--spacing-md);
background-color: #E3F2FD; /* Light blue */
color: #0D47A1; /* Dark blue text */
border: 1px solid #90CAF9;
}
```
2. **Add dark mode override:**
```css
html[data-theme="dark"] .ps-alert-custom {
background-color: #1A237E; /* Dark blue bg */
color: #E3F2FD; /* Light text */
border-color: #3F51B5; /* Darker border */
}
```
3. **Test:**
- Load page in light mode → verify colors
- Toggle dark mode → verify override colors are applied
- Open DevTools → verify `html[data-theme="dark"]` attribute set
- Check localStorage `peptide-starter-theme` is updated
### Helping Plugins Support Dark Mode
**If a plugin doesn't listen to dark mode changes:**
1. Ask plugin author to listen to the `themechange` event:
```javascript
document.addEventListener('themechange', (e) => {
const theme = e.detail.theme;
// Update plugin styles based on theme
});
```
2. Or use a MutationObserver to watch for `data-theme` changes:
```javascript
const observer = new MutationObserver(() => {
const theme = document.documentElement.getAttribute('data-theme');
// Update styles
});
observer.observe(document.documentElement, { attributes: true });
```
3. Or read CSS variables in the plugin stylesheet:
```css
.plugin-card {
color: var(--text-primary);
background-color: var(--bg-secondary);
}
```
### CSS Variables for Dark Mode
All available variables (defined in `:root`):
```css
--text-primary /* Main text (light: #111827, dark: #F9FAFB) */
--text-secondary /* Secondary text (light: #6B7280, dark: #D1D5DB) */
--bg-primary /* Main background (light: #FFFFFF, dark: #111827) */
--bg-secondary /* Secondary bg (light: #F3F4F6, dark: #1F2937) */
--color-primary /* Primary accent (light: #0066CC, dark: #3B82F6) */
--border-color /* Border color (light: #E5E7EB, dark: #374151) */
```
## Testing Checklist
When modifying the theme, verify:
- [ ] **Light mode** — All text readable, colors correct
- [ ] **Dark mode** — Toggle to dark, verify colors swap, no white flash on reload
- [ ] **Mobile** (480px) — Navigation collapses, search/dark mode icons present
- [ ] **Tablet** (768px) — Layout adapts, footer 2-column if needed
- [ ] **Desktop** (1024px+) — Full layout renders correctly
- [ ] **Plugins active** — Shortcodes render, no JS errors in console
- [ ] **Plugins inactive** — Graceful fallbacks work (search form, no broken layout)
- [ ] **Accessibility** — Tab through page, all interactive elements reachable
- [ ] **Focus states** — Every button/link has visible focus outline
- [ ] **Keyboard navigation** — Escape closes overlays, arrow keys in menu if relevant
## Common Tasks & Commands
**Check JavaScript syntax:**
```bash
node --check assets/js/theme.js
node --check assets/js/navigation.js
```
**View theme version:**
```php
echo PEPTIDE_STARTER_VERSION; // Currently: 1.3.1
```
**Update theme version (when releasing):**
1. Edit `style.css` line 7: `Version: X.Y.Z`
2. Edit `functions.php` line 14: `define( 'PEPTIDE_STARTER_VERSION', 'X.Y.Z' )`
**Deploy to production:**
- Push to `main` branch on GitHub
- GitHub Actions runs rsync to Hostinger via SSH
- LiteSpeed cache clears automatically
**Debug active plugins:**
```php
// In functions.php or template
$active = get_option( 'active_plugins' );
error_log( print_r( $active, true ) );
```
**View Customizer settings:**
```php
echo get_theme_mod( 'hero_title' );
echo get_theme_mod( 'dark_mode_default', false );
```
## Related Documentation
- **ARCHITECTURE.md** — System design, data flows, integrations
- **CLAUDE.md** — Developer context and quick reference
- **README.md** — User-facing feature documentation
- **CHANGELOG.md** — Version history

192
assets/js/auth.js Normal file
View File

@@ -0,0 +1,192 @@
/**
* Authentication Page — Form Toggle & Validation
* Peptide Starter Theme
*
* Handles:
* - Toggle between sign-in and register forms
* - Client-side form validation
* - Password strength indicator
* - AJAX form submission to avoid full page reload
*
* @see page-auth.php — renders the auth forms
* @see functions.php — AJAX handlers (ps_auth_login, ps_auth_register)
*
* Conditionally enqueued only on pages using the Sign In / Register template.
*/
(function() {
'use strict';
var loginWrap = document.getElementById('ps-auth-login');
var registerWrap = document.getElementById('ps-auth-register');
var showRegister = document.getElementById('ps-show-register');
var showLogin = document.getElementById('ps-show-login');
var messages = document.getElementById('ps-auth-messages');
var loginForm = document.getElementById('ps-login-form');
var registerForm = document.getElementById('ps-register-form');
if (!loginWrap || !registerWrap) {
return;
}
/**
* Toggle between login and register views.
* @param {string} view - 'login' or 'register'.
*/
function showView(view) {
clearMessages();
if (view === 'register') {
loginWrap.classList.add('ps-auth-form-wrap--hidden');
registerWrap.classList.remove('ps-auth-form-wrap--hidden');
} else {
registerWrap.classList.add('ps-auth-form-wrap--hidden');
loginWrap.classList.remove('ps-auth-form-wrap--hidden');
}
}
if (showRegister) {
showRegister.addEventListener('click', function(e) {
e.preventDefault();
showView('register');
});
}
if (showLogin) {
showLogin.addEventListener('click', function(e) {
e.preventDefault();
showView('login');
});
}
// Check URL hash on load for deep linking.
if (window.location.hash === '#register') {
showView('register');
}
/**
* Display a message in the auth messages area.
* @param {string} text - Message text.
* @param {string} type - 'success', 'error', or 'info'.
*/
function showMessage(text, type) {
if (!messages) {
return;
}
var alertClass = 'ps-alert ps-alert-' + (type || 'info');
messages.innerHTML = '<div class="' + alertClass + '"><p>' + escapeHtml(text) + '</p></div>';
}
function clearMessages() {
if (messages) {
messages.innerHTML = '';
}
}
/**
* Escape HTML entities to prevent XSS in message display.
* @param {string} str - Raw string.
* @returns {string} Escaped string.
*/
function escapeHtml(str) {
var div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
/**
* Password strength indicator.
* Updates the #ps-password-strength element with a rating.
*/
var passwordInput = document.getElementById('ps-reg-password');
var strengthEl = document.getElementById('ps-password-strength');
if (passwordInput && strengthEl) {
passwordInput.addEventListener('input', function() {
var val = passwordInput.value;
var score = 0;
var label = '';
if (val.length >= 8) { score++; }
if (val.length >= 12) { score++; }
if (/[a-z]/.test(val) && /[A-Z]/.test(val)) { score++; }
if (/\d/.test(val)) { score++; }
if (/[^a-zA-Z0-9]/.test(val)) { score++; }
if (val.length === 0) {
label = '';
} else if (score <= 1) {
label = '<span class="ps-strength-weak">Weak</span>';
} else if (score <= 3) {
label = '<span class="ps-strength-fair">Fair</span>';
} else {
label = '<span class="ps-strength-strong">Strong</span>';
}
strengthEl.innerHTML = label;
});
}
/**
* AJAX form submission handler.
* @param {HTMLFormElement} form - The form to submit.
*/
function submitForm(form) {
var formData = new FormData(form);
var btn = form.querySelector('button[type="submit"]');
var origText = btn.textContent;
btn.disabled = true;
btn.textContent = '...';
clearMessages();
var xhr = new XMLHttpRequest();
xhr.open('POST', (typeof peptideStarterData !== 'undefined' ? peptideStarterData.ajaxUrl : '/wp-admin/admin-ajax.php'));
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.onload = function() {
btn.disabled = false;
btn.textContent = origText;
try {
var resp = JSON.parse(xhr.responseText);
if (resp.success && resp.data && resp.data.redirect) {
window.location.href = resp.data.redirect;
} else {
showMessage(resp.data && resp.data.message ? resp.data.message : 'An error occurred.', 'error');
}
} catch (e) {
showMessage('An unexpected error occurred. Please try again.', 'error');
}
};
xhr.onerror = function() {
btn.disabled = false;
btn.textContent = origText;
showMessage('Network error. Please check your connection.', 'error');
};
xhr.send(formData);
}
if (loginForm) {
loginForm.addEventListener('submit', function(e) {
e.preventDefault();
if (!loginForm.checkValidity()) {
loginForm.reportValidity();
return;
}
submitForm(loginForm);
});
}
if (registerForm) {
registerForm.addEventListener('submit', function(e) {
e.preventDefault();
if (!registerForm.checkValidity()) {
registerForm.reportValidity();
return;
}
submitForm(registerForm);
});
}
})();

145
assets/js/documentation.js Normal file
View File

@@ -0,0 +1,145 @@
/**
* Documentation Page — Table of Contents & Scroll Spy
* Peptide Starter Theme
*
* Handles:
* - Auto-generates ToC from h2 headings in the documentation body
* - Scroll spy highlights the active section in the sidebar
* - Smooth scrolling on ToC link click
* - Collapses sidebar to dropdown on mobile
*
* @see page-documentation.php — renders the two-column layout
* @see style.css — .ps-docs-* classes
*
* Conditionally enqueued only on pages using the Documentation template.
*/
(function() {
'use strict';
const docsBody = document.getElementById('ps-docs-body');
const tocList = document.getElementById('ps-docs-toc-list');
const sidebar = document.querySelector('.ps-docs-sidebar');
if (!docsBody || !tocList) {
return;
}
const headings = docsBody.querySelectorAll('h2');
if (headings.length === 0) {
// Hide the sidebar if there are no headings to navigate.
if (sidebar) {
sidebar.style.display = 'none';
}
return;
}
/**
* Build the Table of Contents from h2 headings.
* Assigns IDs to headings that lack one for anchor linking.
*/
function buildToc() {
headings.forEach(function(heading, index) {
// Ensure every heading has an ID for anchor links.
if (!heading.id) {
heading.id = 'section-' + (index + 1);
}
var li = document.createElement('li');
var link = document.createElement('a');
link.href = '#' + heading.id;
link.textContent = heading.textContent;
link.className = 'ps-docs-toc-link';
link.addEventListener('click', function(e) {
e.preventDefault();
smoothScrollTo(heading);
});
li.appendChild(link);
tocList.appendChild(li);
});
}
/**
* Smooth scroll to a target element, accounting for the sticky header.
* @param {HTMLElement} target The element to scroll to.
*/
function smoothScrollTo(target) {
var headerHeight = document.querySelector('.site-header')
? document.querySelector('.site-header').offsetHeight
: 0;
var offset = 24; // Extra breathing room below header.
var top = target.getBoundingClientRect().top + window.pageYOffset - headerHeight - offset;
window.scrollTo({
top: top,
behavior: 'smooth'
});
// Update URL hash without jumping.
history.replaceState(null, '', '#' + target.id);
}
/**
* Scroll spy — highlights the ToC link corresponding to the
* currently visible section. Uses IntersectionObserver for performance.
*/
function initScrollSpy() {
var tocLinks = tocList.querySelectorAll('.ps-docs-toc-link');
if (tocLinks.length === 0) {
return;
}
var headerHeight = document.querySelector('.site-header')
? document.querySelector('.site-header').offsetHeight
: 0;
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
var id = entry.target.id;
tocLinks.forEach(function(link) {
link.classList.remove('ps-docs-toc-link--active');
if (link.getAttribute('href') === '#' + id) {
link.classList.add('ps-docs-toc-link--active');
}
});
}
});
}, {
rootMargin: '-' + (headerHeight + 32) + 'px 0px -60% 0px',
threshold: 0
});
headings.forEach(function(heading) {
observer.observe(heading);
});
}
/**
* Mobile dropdown toggle for the sidebar ToC.
*/
function initMobileToggle() {
var tocTitle = document.querySelector('.ps-docs-toc-title');
if (!tocTitle) {
return;
}
tocTitle.addEventListener('click', function() {
// Only toggle on mobile (sidebar is not sticky).
if (window.innerWidth < 1024) {
sidebar.classList.toggle('ps-docs-sidebar--open');
}
});
}
// Initialize.
buildToc();
initScrollSpy();
initMobileToggle();
})();

163
assets/js/settings-panel.js Normal file
View File

@@ -0,0 +1,163 @@
/**
* Settings Panel — Open/Close & Contact Form
* Peptide Starter Theme
*
* Handles:
* - Toggle settings slide-out panel visibility
* - AJAX submission of the contact form
* - Overlay backdrop click to close
* - Escape key to close
* - Focus trap inside the panel for accessibility
*
* @see template-parts/settings-panel.php — panel HTML
* @see header.php — settings icon trigger button
* @see functions.php — AJAX handler for contact form
*
* Enqueued globally (lightweight — all listeners are passive until panel opens).
*/
(function() {
'use strict';
var panel = document.getElementById('ps-settings-panel');
var overlay = document.getElementById('ps-settings-overlay');
var closeBtn = document.getElementById('ps-settings-close');
var openBtn = document.getElementById('ps-settings-toggle');
var contactForm = document.getElementById('ps-contact-form');
var statusEl = document.getElementById('ps-contact-status');
if (!panel || !overlay) {
return;
}
/**
* Open the settings panel.
*/
function openPanel() {
panel.classList.add('ps-settings-panel--open');
panel.setAttribute('aria-hidden', 'false');
overlay.classList.add('ps-settings-overlay--active');
overlay.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
// Focus the close button for keyboard users.
if (closeBtn) {
setTimeout(function() { closeBtn.focus(); }, 100);
}
}
/**
* Close the settings panel.
*/
function closePanel() {
panel.classList.remove('ps-settings-panel--open');
panel.setAttribute('aria-hidden', 'true');
overlay.classList.remove('ps-settings-overlay--active');
overlay.setAttribute('aria-hidden', 'true');
document.body.style.overflow = '';
// Return focus to the trigger button.
if (openBtn) {
openBtn.focus();
}
}
// Bind open trigger.
if (openBtn) {
openBtn.addEventListener('click', openPanel);
}
// Bind close button.
if (closeBtn) {
closeBtn.addEventListener('click', closePanel);
}
// Bind overlay backdrop click.
overlay.addEventListener('click', closePanel);
// Close on Escape key.
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && panel.classList.contains('ps-settings-panel--open')) {
closePanel();
}
});
/**
* Contact form AJAX submission.
*/
if (contactForm) {
contactForm.addEventListener('submit', function(e) {
e.preventDefault();
if (!contactForm.checkValidity()) {
contactForm.reportValidity();
return;
}
var formData = new FormData(contactForm);
var btn = contactForm.querySelector('button[type="submit"]');
var origText = btn.textContent;
btn.disabled = true;
btn.textContent = '...';
if (statusEl) {
statusEl.innerHTML = '';
}
var xhr = new XMLHttpRequest();
var ajaxUrl = (typeof peptideStarterData !== 'undefined' && peptideStarterData.ajaxUrl)
? peptideStarterData.ajaxUrl
: '/wp-admin/admin-ajax.php';
xhr.open('POST', ajaxUrl);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.onload = function() {
btn.disabled = false;
btn.textContent = origText;
try {
var resp = JSON.parse(xhr.responseText);
if (resp.success) {
if (statusEl) {
statusEl.innerHTML = '<div class="ps-alert ps-alert-success"><p>' +
escapeHtml(resp.data.message || 'Message sent.') + '</p></div>';
}
contactForm.reset();
} else {
if (statusEl) {
statusEl.innerHTML = '<div class="ps-alert ps-alert-error"><p>' +
escapeHtml(resp.data.message || 'Failed to send.') + '</p></div>';
}
}
} catch (ex) {
if (statusEl) {
statusEl.innerHTML = '<div class="ps-alert ps-alert-error"><p>An unexpected error occurred.</p></div>';
}
}
};
xhr.onerror = function() {
btn.disabled = false;
btn.textContent = origText;
if (statusEl) {
statusEl.innerHTML = '<div class="ps-alert ps-alert-error"><p>Network error. Please try again.</p></div>';
}
};
xhr.send(formData);
});
}
/**
* Escape HTML for safe insertion.
* @param {string} str - Raw text.
* @returns {string} Escaped HTML.
*/
function escapeHtml(str) {
var div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
})();

View File

@@ -2,46 +2,33 @@
/**
* The footer for Peptide Starter theme
*
* Updated footer with disclaimer, language links, and settings panel.
*
* @see template-parts/settings-panel.php — slide-out support panel
* @see template-parts/newsletter-signup.php — newsletter CTA (on front page)
* @see functions.php — newsletter handler, contact form handler
*
* @package peptide-starter
*/
?>
</main><!-- #main -->
<!-- Newsletter Signup (front page only — science feed includes its own) -->
<?php if ( is_front_page() ) : ?>
<?php get_template_part( 'template-parts/newsletter', 'signup' ); ?>
<?php endif; ?>
<footer class="site-footer">
<div class="footer-wrapper">
<!-- Newsletter Signup -->
<?php if ( peptide_starter_show_newsletter_form() ) { ?>
<div class="footer-newsletter">
<h3><?php esc_html_e( 'Stay Updated', 'peptide-starter' ); ?></h3>
<p><?php esc_html_e( 'Subscribe to our newsletter for the latest peptide research and updates.', 'peptide-starter' ); ?></p>
<form class="newsletter-form" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post">
<?php wp_nonce_field( 'peptide_starter_newsletter', 'ps_newsletter_nonce' ); ?>
<input type="hidden" name="action" value="peptide_starter_newsletter_signup">
<input
type="email"
name="email"
class="newsletter-input"
placeholder="<?php esc_attr_e( 'your@email.com', 'peptide-starter' ); ?>"
required
>
<button type="submit" class="ps-btn ps-btn-primary">
<?php esc_html_e( 'Subscribe', 'peptide-starter' ); ?>
</button>
</form>
</div>
<?php } ?>
<!-- Footer Grid -->
<div class="footer-grid">
<!-- Footer Widget Areas -->
<?php
for ( $i = 1; $i <= 4; $i++ ) {
echo '<div class="footer-section">';
if ( is_active_sidebar( 'footer-' . $i ) ) {
dynamic_sidebar( 'footer-' . $i );
} else {
// Default footer content if no widgets
// Default footer content if no widgets.
if ( 1 === $i ) {
echo '<h3>' . esc_html__( 'About', 'peptide-starter' ) . '</h3>';
echo '<p>' . esc_html__( 'Peptide Repo is a comprehensive scientific database for peptide research.', 'peptide-starter' ) . '</p>';
@@ -50,18 +37,20 @@
echo '<ul>';
echo '<li><a href="' . esc_url( home_url( '/' ) ) . '">' . esc_html__( 'Home', 'peptide-starter' ) . '</a></li>';
echo '<li><a href="' . esc_url( home_url( '/peptides' ) ) . '">' . esc_html__( 'Peptides Database', 'peptide-starter' ) . '</a></li>';
echo '<li><a href="' . esc_url( home_url( '/documentation' ) ) . '">' . esc_html__( 'Documentation', 'peptide-starter' ) . '</a></li>';
echo '<li><a href="' . esc_url( home_url( '/news' ) ) . '">' . esc_html__( 'Science Feed', 'peptide-starter' ) . '</a></li>';
echo '</ul>';
} elseif ( 3 === $i ) {
echo '<h3>' . esc_html__( 'Resources', 'peptide-starter' ) . '</h3>';
echo '<h3>' . esc_html__( 'Tools', 'peptide-starter' ) . '</h3>';
echo '<ul>';
echo '<li><a href="#">' . esc_html__( 'Research', 'peptide-starter' ) . '</a></li>';
echo '<li><a href="#">' . esc_html__( 'Documentation', 'peptide-starter' ) . '</a></li>';
echo '<li><a href="' . esc_url( home_url( '/calculator' ) ) . '">' . esc_html__( 'Calculator', 'peptide-starter' ) . '</a></li>';
echo '<li><a href="' . esc_url( home_url( '/protocol-builder' ) ) . '">' . esc_html__( 'Protocol Builder', 'peptide-starter' ) . '</a></li>';
echo '<li><a href="' . esc_url( home_url( '/tracker' ) ) . '">' . esc_html__( 'Tracker', 'peptide-starter' ) . '</a></li>';
echo '</ul>';
} elseif ( 4 === $i ) {
echo '<h3>' . esc_html__( 'Connect', 'peptide-starter' ) . '</h3>';
echo '<ul>';
echo '<li><a href="https://github.com/peptiderepo" target="_blank" rel="noopener">' . esc_html__( 'GitHub', 'peptide-starter' ) . '</a></li>';
echo '<li><a href="#">' . esc_html__( 'Twitter', 'peptide-starter' ) . '</a></li>';
echo '</ul>';
}
}
@@ -72,11 +61,40 @@
<!-- Footer Bottom -->
<div class="footer-bottom">
<p><?php echo wp_kses_post( peptide_starter_get_footer_copyright() ); ?></p>
<p class="footer-copyright">
<?php
echo wp_kses_post(
sprintf(
/* translators: %s: current year */
__( '&copy; %s Research Directory | Laboratory Standards', 'peptide-starter' ),
gmdate( 'Y' )
)
);
?>
</p>
<!-- Disclaimer -->
<p class="footer-disclaimer">
<?php esc_html_e( 'All data is provided for informational and educational purposes only. This platform does not provide medical advice, diagnoses, or treatment recommendations. Always consult qualified professionals.', 'peptide-starter' ); ?>
</p>
<!-- Language Links (placeholder) -->
<div class="footer-languages">
<span class="footer-languages-label"><?php esc_html_e( 'Our Blogs:', 'peptide-starter' ); ?></span>
<a href="#" hreflang="en"><?php esc_html_e( 'English', 'peptide-starter' ); ?></a>
<a href="#" hreflang="el"><?php esc_html_e( 'Greek', 'peptide-starter' ); ?></a>
<a href="#" hreflang="es"><?php esc_html_e( 'Spanish', 'peptide-starter' ); ?></a>
<a href="#" hreflang="nl"><?php esc_html_e( 'Dutch', 'peptide-starter' ); ?></a>
</div>
</div>
</div>
</footer>
<?php
// Settings slide-out panel (hidden by default, toggled via JS).
get_template_part( 'template-parts/settings', 'panel' );
?>
<?php wp_footer(); ?>
</body>
</html>

View File

@@ -2,6 +2,13 @@
/**
* The front page template
*
* Layout: Hero → Research Modules → PRAutoBlogger posts → Peptide News feed.
* Newsletter signup is handled by footer.php (via template part) on the front page.
*
* @see template-parts/module-cards.php — research modules grid
* @see template-parts/newsletter-signup.php — included via footer.php on front page
* @see functions.php — helper functions for hero content
*
* @package peptide-starter
*/
@@ -15,7 +22,7 @@ get_header();
<h1 class="hero-title">
<?php
$hero_title = peptide_starter_get_hero_title();
// Wrap the last word in an accent span for the wavy underline effect
// Wrap the last word in an accent span for the wavy underline effect.
$words = explode( ' ', $hero_title );
if ( count( $words ) > 1 ) {
$last_word = array_pop( $words );
@@ -53,13 +60,16 @@ get_header();
<a href="<?php echo esc_url( home_url( '/peptides' ) ); ?>" class="ps-btn ps-btn-primary">
<?php esc_html_e( 'Browse Peptides', 'peptide-starter' ); ?>
</a>
<a href="<?php echo esc_url( home_url( '/about' ) ); ?>" class="ps-btn ps-btn-secondary">
<?php esc_html_e( 'Learn More', 'peptide-starter' ); ?>
<a href="<?php echo esc_url( home_url( '/documentation' ) ); ?>" class="ps-btn ps-btn-secondary">
<?php esc_html_e( 'Documentation', 'peptide-starter' ); ?>
</a>
</div>
</div>
</section>
<!-- Research Modules Grid (6 cards) -->
<?php get_template_part( 'template-parts/module', 'cards' ); ?>
<!-- PRAutoBlogger Posts Widget -->
<?php
if ( shortcode_exists( 'prautoblogger_posts' ) ) {
@@ -69,7 +79,6 @@ get_header();
<!-- News Feed Section -->
<?php
// Display the Peptide News plugin shortcode if available
if ( shortcode_exists( 'peptide_news' ) ) {
?>
<section class="news-feed-section">
@@ -86,12 +95,6 @@ get_header();
<?php
}
?>
<?php
// Note: The front page layout is handled entirely by this template.
// Old page content (e.g. from Elementor) is intentionally NOT rendered here
// to avoid duplicate hero sections and empty space.
?>
</main>
<?php get_footer(); ?>

View File

@@ -2,42 +2,50 @@
/**
* Peptide Starter Theme Functions
*
* Core theme setup: supports, menus, scripts, customizer, helpers.
* Feature-specific logic is split into inc/ files to keep this under 300 lines.
*
* @see inc/auth-handlers.php — AJAX login/register handlers
* @see inc/contact-handler.php — AJAX contact form handler
* @see inc/page-setup.php — auto-create pages on theme activation
* @see inc/newsletter-admin.php — admin page for subscriber export
*
* @package peptide-starter
*/
// Exit if accessed directly
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Define constants
define( 'PEPTIDE_STARTER_VERSION', '1.4.1' );
// Define constants.
define( 'PEPTIDE_STARTER_VERSION', '1.5.0' );
define( 'PEPTIDE_STARTER_DIR', get_template_directory() );
define( 'PEPTIDE_STARTER_URI', get_template_directory_uri() );
// Load feature modules.
require_once PEPTIDE_STARTER_DIR . '/inc/helpers.php';
require_once PEPTIDE_STARTER_DIR . '/inc/customizer.php';
require_once PEPTIDE_STARTER_DIR . '/inc/auth-handlers.php';
require_once PEPTIDE_STARTER_DIR . '/inc/contact-handler.php';
require_once PEPTIDE_STARTER_DIR . '/inc/page-setup.php';
require_once PEPTIDE_STARTER_DIR . '/inc/newsletter-admin.php';
/**
* Set up theme defaults and register support for various WordPress features
* Set up theme defaults and register support for various WordPress features.
*
* @return void
*/
function peptide_starter_setup() {
// Add theme support
add_theme_support( 'title-tag' );
add_theme_support( 'post-thumbnails' );
add_theme_support( 'responsive-embeds' );
add_theme_support( 'custom-logo' );
add_theme_support(
'html5',
array(
'search-form',
'comment-form',
'comment-list',
'gallery',
'caption',
'style',
'script',
)
array( 'search-form', 'comment-form', 'comment-list', 'gallery', 'caption', 'style', 'script' )
);
// Register navigation menus
register_nav_menus(
array(
'primary' => esc_html__( 'Primary Menu', 'peptide-starter' ),
@@ -45,36 +53,42 @@ function peptide_starter_setup() {
)
);
// Add support for wide blocks
add_theme_support( 'align-wide' );
// Load text domain
load_theme_textdomain( 'peptide-starter', PEPTIDE_STARTER_DIR . '/languages' );
}
add_action( 'after_setup_theme', 'peptide_starter_setup' );
/**
* Register and enqueue stylesheets and scripts
* Register and enqueue stylesheets and scripts.
*
* @return void
*/
function peptide_starter_scripts() {
// Register and enqueue main stylesheet
wp_enqueue_style( 'peptide-starter-style', PEPTIDE_STARTER_URI . '/style.css', array(), PEPTIDE_STARTER_VERSION );
// Add conditional scripts
wp_enqueue_script( 'peptide-starter-navigation', PEPTIDE_STARTER_URI . '/assets/js/navigation.js', array(), PEPTIDE_STARTER_VERSION, true );
wp_enqueue_script( 'peptide-starter-theme', PEPTIDE_STARTER_URI . '/assets/js/theme.js', array(), PEPTIDE_STARTER_VERSION, true );
wp_enqueue_script( 'peptide-starter-settings-panel', PEPTIDE_STARTER_URI . '/assets/js/settings-panel.js', array(), PEPTIDE_STARTER_VERSION, true );
// Pass PHP data to JavaScript
wp_localize_script(
'peptide-starter-theme',
'peptideStarterData',
array(
'siteUrl' => home_url( '/' ),
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'isDarkMode' => get_theme_mod( 'dark_mode_default', false ),
)
);
// Dequeue comment reply script if comments are not open
// Conditional scripts for specific page templates.
if ( is_page_template( 'page-documentation.php' ) ) {
wp_enqueue_script( 'peptide-starter-docs', PEPTIDE_STARTER_URI . '/assets/js/documentation.js', array(), PEPTIDE_STARTER_VERSION, true );
}
if ( is_page_template( 'page-auth.php' ) ) {
wp_enqueue_script( 'peptide-starter-auth', PEPTIDE_STARTER_URI . '/assets/js/auth.js', array(), PEPTIDE_STARTER_VERSION, true );
}
if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
wp_enqueue_script( 'comment-reply' );
}
@@ -82,14 +96,14 @@ function peptide_starter_scripts() {
add_action( 'wp_enqueue_scripts', 'peptide_starter_scripts' );
/**
* Enqueue plugin override styles at a late priority so they load AFTER plugin stylesheets.
* This ensures theme overrides (e.g. removing line-clamp on article excerpts) beat plugin CSS.
* Enqueue plugin override styles at late priority.
*
* @return void
*/
function peptide_starter_plugin_overrides() {
wp_add_inline_style(
'peptide-starter-style',
'
/* Override Peptide News plugin line-clamp — show full summaries */
p.pn-article-excerpt,
.pn-article-card p.pn-article-excerpt,
body .pn-article-excerpt {
@@ -104,50 +118,19 @@ function peptide_starter_plugin_overrides() {
add_action( 'wp_enqueue_scripts', 'peptide_starter_plugin_overrides', 99 );
/**
* Register widget areas
* Register widget areas.
*
* @return void
*/
function peptide_starter_widgets_init() {
for ( $i = 1; $i <= 4; $i++ ) {
register_sidebar(
array(
'name' => esc_html__( 'Footer - Column 1', 'peptide-starter' ),
'id' => 'footer-1',
'description' => esc_html__( 'Footer widget area 1', 'peptide-starter' ),
'before_widget' => '<div id="%1$s" class="widget %2$s footer-widget">',
'after_widget' => '</div>',
'before_title' => '<h3 class="widget-title">',
'after_title' => '</h3>',
)
);
register_sidebar(
array(
'name' => esc_html__( 'Footer - Column 2', 'peptide-starter' ),
'id' => 'footer-2',
'description' => esc_html__( 'Footer widget area 2', 'peptide-starter' ),
'before_widget' => '<div id="%1$s" class="widget %2$s footer-widget">',
'after_widget' => '</div>',
'before_title' => '<h3 class="widget-title">',
'after_title' => '</h3>',
)
);
register_sidebar(
array(
'name' => esc_html__( 'Footer - Column 3', 'peptide-starter' ),
'id' => 'footer-3',
'description' => esc_html__( 'Footer widget area 3', 'peptide-starter' ),
'before_widget' => '<div id="%1$s" class="widget %2$s footer-widget">',
'after_widget' => '</div>',
'before_title' => '<h3 class="widget-title">',
'after_title' => '</h3>',
)
);
register_sidebar(
array(
'name' => esc_html__( 'Footer - Column 4', 'peptide-starter' ),
'id' => 'footer-4',
'description' => esc_html__( 'Footer widget area 4', 'peptide-starter' ),
/* translators: %d: footer column number */
'name' => sprintf( esc_html__( 'Footer - Column %d', 'peptide-starter' ), $i ),
'id' => 'footer-' . $i,
/* translators: %d: footer column number */
'description' => sprintf( esc_html__( 'Footer widget area %d', 'peptide-starter' ), $i ),
'before_widget' => '<div id="%1$s" class="widget %2$s footer-widget">',
'after_widget' => '</div>',
'before_title' => '<h3 class="widget-title">',
@@ -155,351 +138,19 @@ function peptide_starter_widgets_init() {
)
);
}
}
add_action( 'widgets_init', 'peptide_starter_widgets_init' );
/**
* Register theme customizer options
*/
function peptide_starter_customize_register( $wp_customize ) {
// Logo 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 Text — allows safe HTML (links, bold, em) via wp_kses_post
$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.
*
* Returns true only for truthy values; everything else becomes false.
* Used as sanitize_callback for the dark_mode_default setting.
*
* @param mixed $checked The raw value from the Customizer.
* @return bool True if checked, false otherwise.
*/
function peptide_starter_sanitize_checkbox( $checked ) {
return ( ( isset( $checked ) && true === $checked ) ? true : false );
}
/**
* Add custom logo support
*/
function peptide_starter_get_custom_logo() {
if ( function_exists( 'get_custom_logo' ) ) {
return get_custom_logo();
}
return '';
}
/**
* Navigation Menu Walker for handling active states
*/
class Peptide_Starter_Nav_Walker extends Walker_Nav_Menu {
public function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) {
if ( isset( $args->item_spacing ) && 'discard' === $args->item_spacing ) {
$t = '';
$n = '';
} else {
$t = "\t";
$n = "\n";
}
$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 ) . '"' : '';
$id = apply_filters( 'nav_menu_item_id', 'menu-item-' . $item->ID, $item, $args, $depth );
$id = $id ? ' id="' . esc_attr( $id ) . '"' : '';
$output .= $indent . '<li' . $id . $class_names . '>';
$atts = array();
$atts['title'] = ! empty( $item->attr_title ) ? $item->attr_title : '';
$atts['target'] = ! empty( $item->target ) ? $item->target : '';
$atts['rel'] = ! empty( $item->xfn ) ? $item->xfn : '';
$atts['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( 'nav_menu_item_title', $item->title, $item, $args, $depth );
$title = apply_filters( 'nav_menu_item_title', $title, $item, $args, $depth );
$link = $args->before;
$link .= '<a' . $attributes . '>';
$link .= $args->link_before . $title . $args->link_after;
$link .= '</a>';
$link .= $args->after;
$output .= apply_filters( 'nav_menu_item_title', $link, $item, $args, $depth );
$output .= '</li>' . $n;
}
}
/**
* Display custom logo
*/
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
*/
function peptide_starter_get_hero_title() {
return get_theme_mod( 'hero_title', get_bloginfo( 'name' ) );
}
/**
* Get hero subtitle
*/
function peptide_starter_get_hero_subtitle() {
return get_theme_mod( 'hero_subtitle', esc_html__( 'A scientific peptide reference database', 'peptide-starter' ) );
}
/**
* Get search placeholder
*/
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
*/
function peptide_starter_get_footer_copyright() {
return get_theme_mod( 'footer_copyright', esc_html__( 'Copyright © 2026 Peptide Repo. All rights reserved.', 'peptide-starter' ) );
}
/**
* Apply dark mode class to body if enabled
*/
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' );
/**
* Register pagination function
*/
function peptide_starter_pagination() {
global $wp_query;
if ( $wp_query->max_num_pages <= 1 ) {
return;
}
$current_page = max( 1, get_query_var( 'paged' ) );
$total_pages = $wp_query->max_num_pages;
echo '<nav class="ps-pagination">';
echo paginate_links(
array(
'base' => get_pagenum_link( 1 ) . '%_%',
'format' => 'page/%#%/',
'current' => $current_page,
'total' => $total_pages,
'prev_text' => esc_html__( '← Previous', 'peptide-starter' ),
'next_text' => esc_html__( 'Next →', 'peptide-starter' ),
'type' => 'plain',
)
);
echo '</nav>';
}
/**
* Get the excerpt
*/
function peptide_starter_get_excerpt( $post_id = 0, $length = 20 ) {
if ( ! $post_id ) {
$post_id = get_the_ID();
}
$excerpt = get_the_excerpt( $post_id );
if ( ! $excerpt ) {
$excerpt = wp_trim_words( get_the_content( $post_id ), $length );
}
return $excerpt;
}
/**
* Check if we should show footer newsletter signup
*/
function peptide_starter_show_newsletter_form() {
return apply_filters( 'peptide_starter_show_newsletter', true );
}
/**
* Handle newsletter signup form submission.
* Stores email in wp_options with duplicate check.
*
* Validates the nonce and email, then fires an action hook so plugins
* can handle the actual subscription logic (e.g. Mailchimp, SendGrid).
* If no plugin handles it, redirects back with a generic success message.
*
* @see footer.php — renders the newsletter form
* @return void Redirects back with status parameter.
*/
function peptide_starter_handle_newsletter_signup() {
// Verify nonce — prevents CSRF attacks.
if ( ! isset( $_POST['ps_newsletter_nonce'] ) ||
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['ps_newsletter_nonce'] ) ), 'peptide_starter_newsletter' ) ) {
wp_die(
esc_html__( 'Security check failed. Please try again.', 'peptide-starter' ),
esc_html__( 'Error', 'peptide-starter' ),
array( 'response' => 403 )
);
wp_die( esc_html__( 'Security check failed.', 'peptide-starter' ), esc_html__( 'Error', 'peptide-starter' ), array( 'response' => 403 ) );
}
$email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
@@ -509,13 +160,21 @@ function peptide_starter_handle_newsletter_signup() {
exit;
}
/**
* Fires when a valid newsletter signup is submitted.
*
* Plugins should hook here to send the email to their mailing list provider.
*
* @param string $email The validated subscriber email address.
*/
// Store in wp_options with duplicate check.
$emails = get_option( 'ps_newsletter_emails', array() );
$existing_emails = array_column( $emails, 'email' );
if ( in_array( $email, $existing_emails, true ) ) {
wp_safe_redirect( add_query_arg( 'ps_newsletter', 'duplicate', wp_get_referer() ) );
exit;
}
$emails[] = array(
'email' => $email,
'date' => current_time( 'Y-m-d H:i:s' ),
);
update_option( 'ps_newsletter_emails', $emails );
do_action( 'peptide_starter_newsletter_subscribe', $email );
wp_safe_redirect( add_query_arg( 'ps_newsletter', 'success', wp_get_referer() ) );
@@ -525,29 +184,23 @@ add_action( 'admin_post_peptide_starter_newsletter_signup', 'peptide_starter_han
add_action( 'admin_post_nopriv_peptide_starter_newsletter_signup', 'peptide_starter_handle_newsletter_signup' );
/**
* Enqueue inline script for dark mode toggle (output in footer)
* Inline dark mode script in wp_head — prevents flash of wrong theme.
*
* @return void
*/
function peptide_starter_inline_dark_mode_script() {
$default_dark = get_theme_mod( 'dark_mode_default', false ) ? 'dark' : 'light';
?>
<script>
(function() {
const html = document.documentElement;
const stored = localStorage.getItem('peptide-starter-theme');
const default_theme = <?php echo wp_json_encode( $default_dark ); ?>;
const system_dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
let theme = stored || (system_dark ? 'dark' : default_theme);
var html = document.documentElement;
var stored = localStorage.getItem('peptide-starter-theme');
var defaultTheme = <?php echo wp_json_encode( $default_dark ); ?>;
var systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
var theme = stored || (systemDark ? 'dark' : defaultTheme);
html.setAttribute('data-theme', theme);
})();
</script>
<?php
}
add_action( 'wp_head', 'peptide_starter_inline_dark_mode_script' );
/**
* Debug helper: check if a plugin is active
*/
function peptide_starter_is_plugin_active( $plugin ) {
return in_array( $plugin, (array) get_option( 'active_plugins', array() ), true );
}

View File

@@ -2,6 +2,14 @@
/**
* The header for Peptide Starter theme
*
* Updated navigation: Home | Tools ▾ | My Data ▾ | Resources ▾ | [Sign In] ⚙ 🌐
* Sign In / User menu is state-aware (logged in vs logged out).
*
* @see functions.php — nav menu registration, walker
* @see assets/js/navigation.js — mobile menu toggle
* @see assets/js/settings-panel.js — settings icon trigger
* @see template-parts/settings-panel.php — settings slide-out
*
* @package peptide-starter
*/
@@ -47,10 +55,46 @@
<!-- Header Icons -->
<div class="header-icons">
<!-- Browse Peptides CTA (accent button, hidden on mobile) -->
<a href="<?php echo esc_url( home_url( '/peptides' ) ); ?>" class="ps-btn ps-btn-primary ps-btn-sm ps-header-cta">
<?php esc_html_e( 'Browse Peptides', 'peptide-starter' ); ?>
<!-- Sign In / User Menu (state-aware) -->
<?php if ( is_user_logged_in() ) : ?>
<?php $current_user = wp_get_current_user(); ?>
<div class="ps-user-menu">
<button class="ps-user-menu__toggle" aria-expanded="false" aria-haspopup="true">
<span class="ps-user-menu__name"><?php echo esc_html( $current_user->display_name ); ?></span>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M3 5L6 8L9 5"/>
</svg>
</button>
<div class="ps-user-menu__dropdown" aria-label="<?php esc_attr_e( 'User menu', 'peptide-starter' ); ?>">
<a href="<?php echo esc_url( home_url( '/profile' ) ); ?>"><?php esc_html_e( 'My Profile', 'peptide-starter' ); ?></a>
<a href="<?php echo esc_url( home_url( '/tracker' ) ); ?>"><?php esc_html_e( 'Tracker', 'peptide-starter' ); ?></a>
<a href="<?php echo esc_url( home_url( '/subject-log' ) ); ?>"><?php esc_html_e( 'Subject Log', 'peptide-starter' ); ?></a>
<a href="<?php echo esc_url( wp_logout_url( home_url( '/' ) ) ); ?>"><?php esc_html_e( 'Sign Out', 'peptide-starter' ); ?></a>
</div>
</div>
<?php else : ?>
<a href="<?php echo esc_url( home_url( '/auth' ) ); ?>" class="ps-btn ps-btn-primary ps-btn-sm ps-header-cta">
<?php esc_html_e( 'Sign In', 'peptide-starter' ); ?>
</a>
<?php endif; ?>
<!-- Settings Icon -->
<button class="header-icon-btn" id="ps-settings-toggle" aria-label="<?php esc_attr_e( 'Support & settings', 'peptide-starter' ); ?>">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="10" cy="10" r="3"/>
<path d="M16.474 12.32a1.26 1.26 0 00.252 1.39l.046.046a1.527 1.527 0 11-2.16 2.16l-.046-.046a1.27 1.27 0 00-2.15.9v.13a1.527 1.527 0 11-3.055 0v-.07a1.26 1.26 0 00-.825-1.152 1.26 1.26 0 00-1.39.252l-.046.046a1.527 1.527 0 11-2.16-2.16l.046-.046a1.27 1.27 0 00-.9-2.15h-.13a1.527 1.527 0 110-3.055h.07a1.26 1.26 0 001.152-.825 1.26 1.26 0 00-.252-1.39l-.046-.046a1.527 1.527 0 112.16-2.16l.046.046a1.26 1.26 0 001.39.252h.06a1.26 1.26 0 00.764-1.152v-.13a1.527 1.527 0 113.055 0v.07a1.27 1.27 0 002.15.9l.046-.046a1.527 1.527 0 112.16 2.16l-.046.046a1.26 1.26 0 00-.252 1.39v.06a1.26 1.26 0 001.152.764h.13a1.527 1.527 0 110 3.055h-.07a1.26 1.26 0 00-1.152.825z"/>
</svg>
</button>
<!-- Language Placeholder -->
<span class="ps-lang-placeholder" aria-label="<?php esc_attr_e( 'Language', 'peptide-starter' ); ?>" title="<?php esc_attr_e( 'Language selection coming soon', 'peptide-starter' ); ?>">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="10" cy="10" r="8"/>
<line x1="2" y1="10" x2="18" y2="10"/>
<path d="M10 2a14.5 14.5 0 014 8 14.5 14.5 0 01-4 8 14.5 14.5 0 01-4-8 14.5 14.5 0 014-8z"/>
</svg>
<span class="ps-lang-code">EN</span>
</span>
<!-- Search Icon -->
<button class="header-icon-btn search-toggle" aria-label="<?php esc_attr_e( 'Search', 'peptide-starter' ); ?>">
@@ -112,25 +156,34 @@
<?php
/**
* Fallback menu if none is set
* Fallback menu if none is set — provides the new nav structure.
*/
function peptide_starter_primary_menu_fallback() {
?>
<ul>
<li><a href="<?php echo esc_url( home_url( '/' ) ); ?>"><?php esc_html_e( 'Home', 'peptide-starter' ); ?></a></li>
<?php
$about_page = get_page_by_path( 'about' );
if ( $about_page ) {
echo '<li><a href="' . esc_url( get_permalink( $about_page ) ) . '">' . esc_html__( 'About', 'peptide-starter' ) . '</a></li>';
}
// Display link to Peptides archive
if ( post_type_exists( 'peptide' ) ) {
echo '<li><a href="' . esc_url( get_post_type_archive_link( 'peptide' ) ) . '">' . esc_html__( 'Peptides', 'peptide-starter' ) . '</a></li>';
} else {
// Fallback to /peptides/ URL if CPT not registered yet
echo '<li><a href="' . esc_url( home_url( '/peptides/' ) ) . '">' . esc_html__( 'Peptides', 'peptide-starter' ) . '</a></li>';
}
?>
<li class="menu-item-has-children">
<a href="#"><?php esc_html_e( 'Tools', 'peptide-starter' ); ?></a>
<ul class="sub-menu">
<li><a href="<?php echo esc_url( home_url( '/calculator' ) ); ?>"><?php esc_html_e( 'Calculator', 'peptide-starter' ); ?></a></li>
<li><a href="<?php echo esc_url( home_url( '/protocol-builder' ) ); ?>"><?php esc_html_e( 'Protocol Builder', 'peptide-starter' ); ?></a></li>
<li><a href="<?php echo esc_url( home_url( '/tracker' ) ); ?>"><?php esc_html_e( 'Tracker', 'peptide-starter' ); ?></a></li>
</ul>
</li>
<li class="menu-item-has-children">
<a href="#"><?php esc_html_e( 'My Data', 'peptide-starter' ); ?></a>
<ul class="sub-menu">
<li><a href="<?php echo esc_url( home_url( '/peptides' ) ); ?>"><?php esc_html_e( 'Peptides', 'peptide-starter' ); ?></a></li>
<li><a href="<?php echo esc_url( home_url( '/subject-log' ) ); ?>"><?php esc_html_e( 'Subject Log', 'peptide-starter' ); ?></a></li>
</ul>
</li>
<li class="menu-item-has-children">
<a href="#"><?php esc_html_e( 'Resources', 'peptide-starter' ); ?></a>
<ul class="sub-menu">
<li><a href="<?php echo esc_url( home_url( '/documentation' ) ); ?>"><?php esc_html_e( 'Documentation', 'peptide-starter' ); ?></a></li>
<li><a href="<?php echo esc_url( home_url( '/news' ) ); ?>"><?php esc_html_e( 'Science Feed', 'peptide-starter' ); ?></a></li>
</ul>
</li>
</ul>
<?php
}

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' );

165
page-auth.php Normal file
View File

@@ -0,0 +1,165 @@
<?php
/**
* Template Name: Sign In / Register
*
* Branded authentication page replacing wp-login.php for the frontend.
* Handles sign-in and registration via WordPress native auth functions.
*
* @see assets/js/auth.js — form toggle and client-side validation
* @see functions.php — AJAX handler for auth form submission
*
* What: Renders sign-in / register forms with CSRF protection.
* Who calls it: WordPress template hierarchy when a page uses this template.
* Dependencies: WordPress core auth (wp_signon, wp_create_user).
*
* @package peptide-starter
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Redirect already logged-in users.
if ( is_user_logged_in() ) {
$redirect = isset( $_GET['redirect_to'] ) ? esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ) : home_url( '/profile' );
wp_safe_redirect( $redirect );
exit;
}
// Capture redirect target for post-auth.
$redirect_to = isset( $_GET['redirect_to'] ) ? esc_url( wp_unslash( $_GET['redirect_to'] ) ) : home_url( '/' );
get_header();
?>
<main id="main" class="site-main">
<section class="ps-tool-page ps-auth-page">
<div class="ps-container ps-auth-container">
<div class="ps-auth-card">
<!-- Info Box -->
<div class="ps-alert ps-alert-info ps-auth-info">
<p>
<?php esc_html_e( 'To access the Subject Logs and store assay / lab results and protocols, you must log in or register so we can secure your data.', 'peptide-starter' ); ?>
</p>
</div>
<!-- Auth Status Messages -->
<div id="ps-auth-messages" class="ps-auth-messages" aria-live="polite"></div>
<!-- Sign In Form -->
<div id="ps-auth-login" class="ps-auth-form-wrap">
<h1><?php esc_html_e( 'Sign In', 'peptide-starter' ); ?></h1>
<form id="ps-login-form" class="ps-auth-form" method="post" novalidate>
<?php wp_nonce_field( 'ps_auth_login', 'ps_login_nonce' ); ?>
<input type="hidden" name="action" value="ps_auth_login">
<input type="hidden" name="redirect_to" value="<?php echo esc_attr( $redirect_to ); ?>">
<div class="ps-form-group">
<label for="ps-login-email"><?php esc_html_e( 'Email', 'peptide-starter' ); ?></label>
<input
type="email"
id="ps-login-email"
name="email"
placeholder="<?php esc_attr_e( 'researcher@lab.com', 'peptide-starter' ); ?>"
required
autocomplete="email"
>
</div>
<div class="ps-form-group">
<label for="ps-login-password"><?php esc_html_e( 'Password', 'peptide-starter' ); ?></label>
<input
type="password"
id="ps-login-password"
name="password"
required
autocomplete="current-password"
>
</div>
<button type="submit" class="ps-btn ps-btn-primary ps-btn-full">
<?php esc_html_e( 'Sign In', 'peptide-starter' ); ?>
</button>
<p class="ps-auth-link">
<a href="<?php echo esc_url( wp_lostpassword_url() ); ?>">
<?php esc_html_e( 'Forgot password?', 'peptide-starter' ); ?>
</a>
</p>
</form>
<p class="ps-auth-toggle">
<?php esc_html_e( "Don't have an account?", 'peptide-starter' ); ?>
<a href="#register" id="ps-show-register"><?php esc_html_e( 'Register', 'peptide-starter' ); ?></a>
</p>
</div>
<!-- Register Form (hidden by default) -->
<div id="ps-auth-register" class="ps-auth-form-wrap ps-auth-form-wrap--hidden">
<h1><?php esc_html_e( 'Create Account', 'peptide-starter' ); ?></h1>
<form id="ps-register-form" class="ps-auth-form" method="post" novalidate>
<?php wp_nonce_field( 'ps_auth_register', 'ps_register_nonce' ); ?>
<input type="hidden" name="action" value="ps_auth_register">
<input type="hidden" name="redirect_to" value="<?php echo esc_attr( $redirect_to ); ?>">
<div class="ps-form-group">
<label for="ps-reg-username"><?php esc_html_e( 'Username', 'peptide-starter' ); ?></label>
<input
type="text"
id="ps-reg-username"
name="username"
placeholder="<?php esc_attr_e( '5-15 alphanumeric characters', 'peptide-starter' ); ?>"
required
minlength="5"
maxlength="15"
pattern="[a-zA-Z0-9]+"
autocomplete="username"
>
<span class="ps-form-hint">
<?php esc_html_e( 'This becomes your public username.', 'peptide-starter' ); ?>
</span>
</div>
<div class="ps-form-group">
<label for="ps-reg-email"><?php esc_html_e( 'Email', 'peptide-starter' ); ?></label>
<input
type="email"
id="ps-reg-email"
name="email"
required
autocomplete="email"
>
</div>
<div class="ps-form-group">
<label for="ps-reg-password"><?php esc_html_e( 'Password', 'peptide-starter' ); ?></label>
<input
type="password"
id="ps-reg-password"
name="password"
required
minlength="8"
autocomplete="new-password"
>
<div id="ps-password-strength" class="ps-password-strength" aria-live="polite"></div>
</div>
<button type="submit" class="ps-btn ps-btn-primary ps-btn-full">
<?php esc_html_e( 'Register', 'peptide-starter' ); ?>
</button>
</form>
<p class="ps-auth-toggle">
<?php esc_html_e( 'Already have an account?', 'peptide-starter' ); ?>
<a href="#login" id="ps-show-login"><?php esc_html_e( 'Sign In', 'peptide-starter' ); ?></a>
</p>
</div>
</div>
</div>
</section>
</main>
<?php get_footer(); ?>

48
page-calculator.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
/**
* Template Name: Calculator
*
* Page template for the reconstitution calculator tool.
* Loads the Peptide Tools plugin calculator via shortcode.
*
* @see page-protocol-builder.php — sibling tool template
* @see functions.php — enqueue and page auto-creation
*
* What: Renders the calculator page with plugin shortcode integration.
* Who calls it: WordPress template hierarchy when a page uses this template.
* Dependencies: Peptide Tools plugin (optional — graceful fallback).
*
* @package peptide-starter
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
get_header();
?>
<main id="main" class="site-main">
<section class="ps-tool-page">
<div class="ps-container">
<?php
if ( shortcode_exists( 'peptide_tools_calculator' ) ) {
echo do_shortcode( '[peptide_tools_calculator]' );
} else {
?>
<div class="ps-tool-fallback">
<h1><?php esc_html_e( 'Reconstitution Calculator', 'peptide-starter' ); ?></h1>
<p><?php esc_html_e( 'The calculator tool is currently being set up. Please check back soon.', 'peptide-starter' ); ?></p>
<a href="<?php echo esc_url( home_url( '/' ) ); ?>" class="ps-btn ps-btn-primary">
<?php esc_html_e( 'Return Home', 'peptide-starter' ); ?>
</a>
</div>
<?php
}
?>
</div>
</section>
</main>
<?php get_footer(); ?>

48
page-directory.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
/**
* Template Name: Peptide Directory
*
* Page template for the peptide directory / browsable database.
* Loads the Peptide Community plugin directory via shortcode.
*
* @see archive-peptide.php — CPT archive (different from this directory)
* @see functions.php — enqueue and page auto-creation
*
* What: Renders the peptide directory page with plugin shortcode integration.
* Who calls it: WordPress template hierarchy when a page uses this template.
* Dependencies: Peptide Community plugin (optional — graceful fallback).
*
* @package peptide-starter
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
get_header();
?>
<main id="main" class="site-main">
<section class="ps-tool-page">
<div class="ps-container">
<?php
if ( shortcode_exists( 'peptide_directory' ) ) {
echo do_shortcode( '[peptide_directory]' );
} else {
?>
<div class="ps-tool-fallback">
<h1><?php esc_html_e( 'Peptide Directory', 'peptide-starter' ); ?></h1>
<p><?php esc_html_e( 'The peptide directory is currently being set up. Please check back soon.', 'peptide-starter' ); ?></p>
<a href="<?php echo esc_url( home_url( '/' ) ); ?>" class="ps-btn ps-btn-primary">
<?php esc_html_e( 'Return Home', 'peptide-starter' ); ?>
</a>
</div>
<?php
}
?>
</div>
</section>
</main>
<?php get_footer(); ?>

65
page-documentation.php Normal file
View File

@@ -0,0 +1,65 @@
<?php
/**
* Template Name: Documentation
*
* Page template for the documentation / SOP page.
* Renders WordPress page content with a sticky sidebar ToC generated from
* heading structure via JavaScript (documentation.js).
*
* @see assets/js/documentation.js — ToC generation + scroll spy
* @see functions.php — conditional enqueue of documentation.js
*
* What: Two-column layout (sidebar ToC + long-form content) for documentation.
* Who calls it: WordPress template hierarchy when a page uses this template.
* Dependencies: None (content comes from WordPress editor).
*
* @package peptide-starter
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
get_header();
?>
<main id="main" class="site-main">
<section class="ps-tool-page ps-docs-layout">
<!-- Sidebar Table of Contents (populated by documentation.js) -->
<aside class="ps-docs-sidebar" aria-label="<?php esc_attr_e( 'On this page', 'peptide-starter' ); ?>">
<div class="ps-docs-sidebar-inner">
<h2 class="ps-docs-toc-title"><?php esc_html_e( 'On This Page', 'peptide-starter' ); ?></h2>
<nav class="ps-docs-toc" aria-label="<?php esc_attr_e( 'Table of contents', 'peptide-starter' ); ?>">
<!-- JS populates this from h2 headings in content -->
<ul id="ps-docs-toc-list"></ul>
</nav>
</div>
</aside>
<!-- Main Documentation Content -->
<div class="ps-docs-content">
<div class="ps-container">
<?php
while ( have_posts() ) :
the_post();
?>
<header class="ps-docs-header">
<h1><?php the_title(); ?></h1>
<?php if ( has_excerpt() ) : ?>
<p class="ps-docs-intro"><?php echo esc_html( get_the_excerpt() ); ?></p>
<?php endif; ?>
</header>
<div class="ps-docs-body" id="ps-docs-body">
<?php the_content(); ?>
</div>
<?php
endwhile;
?>
</div>
</div>
</section>
</main>
<?php get_footer(); ?>

63
page-profile.php Normal file
View File

@@ -0,0 +1,63 @@
<?php
/**
* Template Name: User Profile
*
* Page template for the user profile page.
* Loads the Peptide Community plugin profile via shortcode.
*
* @see page-auth.php — authentication template
* @see functions.php — enqueue and page auto-creation
*
* What: Renders the user profile page with plugin shortcode integration.
* Who calls it: WordPress template hierarchy when a page uses this template.
* Dependencies: Peptide Community plugin (optional — graceful fallback).
*
* @package peptide-starter
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Redirect unauthenticated users to the auth page.
if ( ! is_user_logged_in() ) {
wp_safe_redirect( home_url( '/auth?redirect_to=' . rawurlencode( get_permalink() ) ) );
exit;
}
get_header();
?>
<main id="main" class="site-main">
<section class="ps-tool-page">
<div class="ps-container">
<?php
if ( shortcode_exists( 'peptide_community_profile' ) ) {
echo do_shortcode( '[peptide_community_profile]' );
} else {
$current_user = wp_get_current_user();
?>
<div class="ps-profile-basic">
<h1><?php echo esc_html( sprintf( __( 'Welcome, %s', 'peptide-starter' ), $current_user->display_name ) ); ?></h1>
<p><?php esc_html_e( 'Your profile page is being set up. Check back soon for full profile features.', 'peptide-starter' ); ?></p>
<div class="ps-profile-links">
<a href="<?php echo esc_url( home_url( '/tracker' ) ); ?>" class="ps-btn ps-btn-secondary">
<?php esc_html_e( 'Tracker', 'peptide-starter' ); ?>
</a>
<a href="<?php echo esc_url( home_url( '/subject-log' ) ); ?>" class="ps-btn ps-btn-secondary">
<?php esc_html_e( 'Subject Log', 'peptide-starter' ); ?>
</a>
<a href="<?php echo esc_url( wp_logout_url( home_url( '/' ) ) ); ?>" class="ps-btn ps-btn-tertiary">
<?php esc_html_e( 'Sign Out', 'peptide-starter' ); ?>
</a>
</div>
</div>
<?php
}
?>
</div>
</section>
</main>
<?php get_footer(); ?>

48
page-protocol-builder.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
/**
* Template Name: Protocol Builder
*
* Page template for the interactive protocol builder wizard.
* Loads the Peptide Tools plugin protocol builder via shortcode.
*
* @see page-calculator.php — sibling tool template
* @see functions.php — enqueue and page auto-creation
*
* What: Renders the protocol builder page with plugin shortcode integration.
* Who calls it: WordPress template hierarchy when a page uses this template.
* Dependencies: Peptide Tools plugin (optional — graceful fallback).
*
* @package peptide-starter
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
get_header();
?>
<main id="main" class="site-main">
<section class="ps-tool-page">
<div class="ps-container">
<?php
if ( shortcode_exists( 'peptide_tools_protocol_builder' ) ) {
echo do_shortcode( '[peptide_tools_protocol_builder]' );
} else {
?>
<div class="ps-tool-fallback">
<h1><?php esc_html_e( 'Protocol Builder', 'peptide-starter' ); ?></h1>
<p><?php esc_html_e( 'The protocol builder tool is currently being set up. Please check back soon.', 'peptide-starter' ); ?></p>
<a href="<?php echo esc_url( home_url( '/' ) ); ?>" class="ps-btn ps-btn-primary">
<?php esc_html_e( 'Return Home', 'peptide-starter' ); ?>
</a>
</div>
<?php
}
?>
</div>
</section>
</main>
<?php get_footer(); ?>

56
page-science-feed.php Normal file
View File

@@ -0,0 +1,56 @@
<?php
/**
* Template Name: Science Feed
*
* Page template for the science news feed.
* Loads the Peptide News plugin feed via shortcode, plus newsletter signup.
*
* @see front-page.php — also renders [peptide_news] in a different context
* @see template-parts/newsletter-signup.php — included at bottom
*
* What: Renders the science feed page with news shortcode + newsletter CTA.
* Who calls it: WordPress template hierarchy when a page uses this template.
* Dependencies: Peptide News plugin (optional — graceful fallback).
*
* @package peptide-starter
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
get_header();
?>
<main id="main" class="site-main">
<section class="ps-tool-page">
<div class="ps-container">
<?php
if ( shortcode_exists( 'peptide_news' ) ) {
?>
<div class="ps-science-feed-header">
<h1><?php esc_html_e( 'Science Feed', 'peptide-starter' ); ?></h1>
<p><?php esc_html_e( 'Latest research publications, clinical studies, and laboratory insights.', 'peptide-starter' ); ?></p>
</div>
<?php
echo do_shortcode( '[peptide_news]' );
} else {
?>
<div class="ps-tool-fallback">
<h1><?php esc_html_e( 'Science Feed', 'peptide-starter' ); ?></h1>
<p><?php esc_html_e( 'The science feed is currently being set up. Please check back soon.', 'peptide-starter' ); ?></p>
<a href="<?php echo esc_url( home_url( '/' ) ); ?>" class="ps-btn ps-btn-primary">
<?php esc_html_e( 'Return Home', 'peptide-starter' ); ?>
</a>
</div>
<?php
}
?>
</div>
</section>
<?php get_template_part( 'template-parts/newsletter', 'signup' ); ?>
</main>
<?php get_footer(); ?>

48
page-subject-log.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
/**
* Template Name: Subject Log
*
* Page template for the longitudinal subject tracking log.
* Loads the Peptide Tracker plugin subject log via shortcode.
*
* @see page-tracker.php — related tracker template
* @see functions.php — enqueue and page auto-creation
*
* What: Renders the subject log page with plugin shortcode integration.
* Who calls it: WordPress template hierarchy when a page uses this template.
* Dependencies: Peptide Tracker plugin (optional — graceful fallback).
*
* @package peptide-starter
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
get_header();
?>
<main id="main" class="site-main">
<section class="ps-tool-page">
<div class="ps-container">
<?php
if ( shortcode_exists( 'peptide_tracker_subject_log' ) ) {
echo do_shortcode( '[peptide_tracker_subject_log]' );
} else {
?>
<div class="ps-tool-fallback">
<h1><?php esc_html_e( 'Subject Log', 'peptide-starter' ); ?></h1>
<p><?php esc_html_e( 'The subject log tool is currently being set up. Please check back soon.', 'peptide-starter' ); ?></p>
<a href="<?php echo esc_url( home_url( '/' ) ); ?>" class="ps-btn ps-btn-primary">
<?php esc_html_e( 'Return Home', 'peptide-starter' ); ?>
</a>
</div>
<?php
}
?>
</div>
</section>
</main>
<?php get_footer(); ?>

48
page-tracker.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
/**
* Template Name: Tracker
*
* Page template for the compound tracker tool.
* Loads the Peptide Tracker plugin via shortcode.
*
* @see page-subject-log.php — related tracker template
* @see functions.php — enqueue and page auto-creation
*
* What: Renders the tracker page with plugin shortcode integration.
* Who calls it: WordPress template hierarchy when a page uses this template.
* Dependencies: Peptide Tracker plugin (optional — graceful fallback).
*
* @package peptide-starter
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
get_header();
?>
<main id="main" class="site-main">
<section class="ps-tool-page">
<div class="ps-container">
<?php
if ( shortcode_exists( 'peptide_tracker' ) ) {
echo do_shortcode( '[peptide_tracker]' );
} else {
?>
<div class="ps-tool-fallback">
<h1><?php esc_html_e( 'Compound Tracker', 'peptide-starter' ); ?></h1>
<p><?php esc_html_e( 'The tracking tool is currently being set up. Please check back soon.', 'peptide-starter' ); ?></p>
<a href="<?php echo esc_url( home_url( '/' ) ); ?>" class="ps-btn ps-btn-primary">
<?php esc_html_e( 'Return Home', 'peptide-starter' ); ?>
</a>
</div>
<?php
}
?>
</div>
</section>
</main>
<?php get_footer(); ?>

822
style.css
View File

@@ -4,7 +4,7 @@ Theme URI: https://github.com/peptiderepo/peptide-starter
Description: Premium, accessible WordPress theme for scientific peptide reference database
Author: Peptide Repo
Author URI: https://peptiderepo.com
Version: 1.4.1
Version: 1.5.0
License: GPL v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: peptide-starter
@@ -2254,6 +2254,826 @@ html[data-theme="light"] {
flex-wrap: wrap;
}
/* ============================================================================
14. TOOL PAGES — Generic wrapper for plugin shortcode templates
============================================================================ */
.ps-tool-page {
padding: var(--spacing-3xl) var(--spacing-lg);
min-height: 60vh;
}
.ps-tool-fallback {
text-align: center;
padding: var(--spacing-4xl) 0;
}
.ps-tool-fallback h1 {
margin-bottom: var(--spacing-md);
}
.ps-tool-fallback p {
color: var(--color-text-secondary);
margin-bottom: var(--spacing-2xl);
}
/* ============================================================================
15. RESEARCH MODULE CARDS — Front page grid
============================================================================ */
.ps-modules-section {
padding: var(--spacing-3xl) var(--spacing-lg);
background-color: var(--color-bg-secondary);
}
.ps-modules-header {
text-align: center;
margin-bottom: var(--spacing-2xl);
}
.ps-modules-title {
font-size: var(--text-h2);
margin-bottom: var(--spacing-sm);
margin-top: 0;
}
.ps-modules-subtitle {
color: var(--color-text-secondary);
font-size: var(--text-body-md);
}
.ps-modules-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--spacing-lg);
max-width: var(--container-2xl);
margin: 0 auto;
}
.ps-module-card {
display: flex;
flex-direction: column;
padding: var(--spacing-xl);
background-color: var(--color-bg-primary);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-lg);
text-decoration: none;
color: var(--color-text-primary);
transition: all var(--transition-normal);
}
.ps-module-card:hover {
border-color: var(--color-brand-primary);
box-shadow: 0 4px 16px rgba(0, 102, 204, 0.1);
transform: translateY(-2px);
}
.ps-module-card--inactive {
opacity: 0.5;
pointer-events: none;
}
.ps-module-card__icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: 50%;
background-color: rgba(0, 102, 204, 0.1);
color: var(--color-brand-primary);
margin-bottom: var(--spacing-md);
}
.ps-module-card__title {
font-size: var(--text-h4);
font-weight: 600;
margin-bottom: var(--spacing-sm);
margin-top: 0;
}
.ps-module-card__description {
font-size: var(--text-body-md);
color: var(--color-text-secondary);
line-height: 1.6;
flex: 1;
margin-bottom: var(--spacing-md);
}
.ps-module-card__link {
font-size: var(--text-body-sm);
font-weight: 600;
color: var(--color-brand-primary);
}
/* ============================================================================
16. DOCUMENTATION PAGE — Two-column layout with ToC sidebar
============================================================================ */
.ps-docs-layout {
display: grid;
grid-template-columns: 240px 1fr;
gap: var(--spacing-2xl);
max-width: var(--container-2xl);
margin: 0 auto;
align-items: start;
}
.ps-docs-sidebar {
position: sticky;
top: 100px;
max-height: calc(100vh - 120px);
overflow-y: auto;
padding: var(--spacing-lg) 0;
}
.ps-docs-sidebar-inner {
padding-right: var(--spacing-lg);
border-right: 1px solid var(--color-border-subtle);
}
.ps-docs-toc-title {
font-size: var(--text-h6);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--color-text-secondary);
margin-bottom: var(--spacing-md);
margin-top: 0;
cursor: default;
}
.ps-docs-toc ul {
list-style: none;
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.ps-docs-toc-link {
display: block;
padding: var(--spacing-xs) var(--spacing-sm);
font-size: var(--text-body-sm);
color: var(--color-text-secondary);
text-decoration: none;
border-radius: var(--radius-sm);
transition: all var(--transition-fast);
border-left: 2px solid transparent;
}
.ps-docs-toc-link:hover {
color: var(--color-brand-primary);
background-color: var(--color-bg-secondary);
}
.ps-docs-toc-link--active {
color: var(--color-brand-primary);
border-left-color: var(--color-brand-primary);
background-color: var(--color-bg-secondary);
font-weight: 500;
}
.ps-docs-content {
min-width: 0;
}
.ps-docs-header {
margin-bottom: var(--spacing-2xl);
}
.ps-docs-header h1 {
margin-bottom: var(--spacing-md);
}
.ps-docs-intro {
font-size: var(--text-body-lg);
color: var(--color-text-secondary);
line-height: 1.7;
}
.ps-docs-body h2 {
margin-top: var(--spacing-3xl);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--color-border-subtle);
}
.ps-docs-body h2:first-child {
margin-top: 0;
padding-top: 0;
border-top: none;
}
/* Documentation step styling for SOPs */
.ps-docs-step {
counter-increment: step-counter;
position: relative;
padding-left: var(--spacing-2xl);
margin-bottom: var(--spacing-xl);
}
.ps-docs-step::before {
content: counter(step-counter);
position: absolute;
left: 0;
top: 0;
width: 28px;
height: 28px;
background-color: var(--color-brand-primary);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--text-body-sm);
font-weight: 700;
}
/* Warning callout for docs */
.ps-docs-warning {
padding: var(--spacing-lg);
background-color: #FEF3C7;
border-left: 4px solid var(--color-status-warning);
border-radius: var(--radius-md);
margin: var(--spacing-lg) 0;
color: #78350F;
}
/* Documentation tables */
.ps-docs-table {
width: 100%;
border-collapse: collapse;
margin: var(--spacing-lg) 0;
}
.ps-docs-table th,
.ps-docs-table td {
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--color-border-default);
text-align: left;
font-size: var(--text-body-md);
}
.ps-docs-table th {
background-color: var(--color-bg-secondary);
font-weight: 600;
}
/* Mobile: collapse sidebar to dropdown */
@media (max-width: 1023px) {
.ps-docs-layout {
grid-template-columns: 1fr;
}
.ps-docs-sidebar {
position: static;
max-height: none;
border-bottom: 1px solid var(--color-border-subtle);
padding-bottom: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
.ps-docs-sidebar-inner {
border-right: none;
padding-right: 0;
}
.ps-docs-toc-title {
cursor: pointer;
}
.ps-docs-toc-title::after {
content: ' ▾';
}
.ps-docs-toc {
display: none;
}
.ps-docs-sidebar--open .ps-docs-toc {
display: block;
}
}
/* ============================================================================
17. AUTH PAGE — Sign-in / Register forms
============================================================================ */
.ps-auth-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 70vh;
}
.ps-auth-container {
max-width: 440px;
}
.ps-auth-card {
background-color: var(--color-bg-primary);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-lg);
padding: var(--spacing-2xl);
box-shadow: var(--shadow-md);
}
.ps-auth-card h1 {
font-size: var(--text-h3);
margin-bottom: var(--spacing-lg);
}
.ps-auth-info {
margin-bottom: var(--spacing-lg);
}
.ps-auth-info p {
margin-bottom: 0;
font-size: var(--text-body-sm);
}
.ps-auth-form .ps-form-group {
margin-bottom: var(--spacing-md);
}
.ps-auth-link {
text-align: center;
margin-top: var(--spacing-md);
font-size: var(--text-body-sm);
}
.ps-auth-toggle {
text-align: center;
margin-top: var(--spacing-lg);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--color-border-subtle);
font-size: var(--text-body-sm);
color: var(--color-text-secondary);
}
.ps-auth-toggle a {
color: var(--color-brand-primary);
font-weight: 500;
}
.ps-auth-form-wrap--hidden {
display: none;
}
.ps-auth-messages {
margin-bottom: var(--spacing-md);
}
.ps-form-hint {
display: block;
font-size: var(--text-caption);
color: var(--color-text-tertiary);
margin-top: 4px;
}
/* Password strength */
.ps-password-strength {
margin-top: 4px;
font-size: var(--text-caption);
font-weight: 500;
}
.ps-strength-weak { color: var(--color-status-error); }
.ps-strength-fair { color: var(--color-status-warning); }
.ps-strength-strong { color: var(--color-status-success); }
/* ============================================================================
18. NEWSLETTER SECTION — Standalone signup CTA
============================================================================ */
.ps-newsletter-section {
background-color: var(--color-bg-tertiary);
padding: var(--spacing-3xl) var(--spacing-lg);
}
.ps-newsletter-inner {
max-width: var(--container-md);
margin: 0 auto;
text-align: center;
}
.ps-newsletter-title {
font-size: var(--text-h2);
margin-bottom: var(--spacing-sm);
margin-top: 0;
}
.ps-newsletter-subtitle {
color: var(--color-text-secondary);
margin-bottom: var(--spacing-lg);
}
.ps-newsletter-form-row {
display: flex;
gap: var(--spacing-sm);
max-width: 480px;
margin: 0 auto;
}
.ps-newsletter-input {
flex: 1;
min-width: 0;
}
.ps-newsletter-status {
margin-top: var(--spacing-md);
text-align: left;
max-width: 480px;
margin-left: auto;
margin-right: auto;
}
.ps-newsletter-learn-more {
margin-top: var(--spacing-md);
font-size: var(--text-body-sm);
}
.ps-newsletter-learn-more a {
color: var(--color-brand-primary);
}
@media (max-width: 479px) {
.ps-newsletter-form-row {
flex-direction: column;
}
}
/* ============================================================================
19. USER MENU — Logged-in dropdown in header
============================================================================ */
.ps-user-menu {
position: relative;
}
.ps-user-menu__toggle {
display: flex;
align-items: center;
gap: var(--spacing-xs);
background: none;
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--text-body-sm);
font-weight: 500;
color: var(--color-text-primary);
cursor: pointer;
transition: all var(--transition-fast);
}
.ps-user-menu__toggle:hover {
border-color: var(--color-brand-primary);
}
.ps-user-menu__name {
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ps-user-menu__dropdown {
display: none;
position: absolute;
top: 100%;
right: 0;
margin-top: var(--spacing-xs);
background-color: var(--color-bg-primary);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
min-width: 180px;
z-index: var(--z-dropdown);
padding: var(--spacing-xs) 0;
}
.ps-user-menu:hover .ps-user-menu__dropdown,
.ps-user-menu__toggle:focus + .ps-user-menu__dropdown {
display: block;
}
.ps-user-menu__dropdown a {
display: block;
padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--text-body-sm);
color: var(--color-text-primary);
text-decoration: none;
transition: background-color var(--transition-fast);
}
.ps-user-menu__dropdown a:hover {
background-color: var(--color-bg-secondary);
color: var(--color-brand-primary);
}
/* ============================================================================
20. SETTINGS PANEL — Slide-out support panel
============================================================================ */
.ps-settings-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
z-index: var(--z-modal-backdrop);
opacity: 0;
visibility: hidden;
transition: opacity var(--transition-slow), visibility var(--transition-slow);
}
.ps-settings-overlay--active {
opacity: 1;
visibility: visible;
}
.ps-settings-panel {
position: fixed;
top: 0;
right: -400px;
width: 380px;
max-width: 90vw;
height: 100vh;
background-color: var(--color-bg-primary);
border-left: 1px solid var(--color-border-default);
box-shadow: var(--shadow-xl);
z-index: var(--z-modal);
overflow-y: auto;
transition: right var(--transition-slow);
display: flex;
flex-direction: column;
}
.ps-settings-panel--open {
right: 0;
}
.ps-settings-panel__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-lg) var(--spacing-xl);
border-bottom: 1px solid var(--color-border-subtle);
}
.ps-settings-panel__header h2 {
font-size: var(--text-h4);
margin: 0;
}
.ps-settings-panel__close {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
border-radius: var(--radius-sm);
transition: all var(--transition-fast);
}
.ps-settings-panel__close:hover {
color: var(--color-text-primary);
background-color: var(--color-bg-secondary);
}
.ps-settings-panel__body {
padding: var(--spacing-xl);
flex: 1;
}
.ps-settings-section {
margin-bottom: var(--spacing-2xl);
}
.ps-settings-section h3 {
font-size: var(--text-h5);
margin-bottom: var(--spacing-md);
margin-top: 0;
}
.ps-settings-disclaimer {
font-size: var(--text-body-sm);
color: var(--color-text-secondary);
}
.ps-settings-disclaimer p {
font-size: var(--text-body-sm);
line-height: 1.6;
}
.ps-contact-form .ps-form-group {
margin-bottom: var(--spacing-md);
}
.ps-contact-status {
margin-top: var(--spacing-md);
}
/* ============================================================================
21. LANGUAGE PLACEHOLDER — Header icon
============================================================================ */
.ps-lang-placeholder {
display: flex;
align-items: center;
gap: 2px;
color: var(--color-text-secondary);
font-size: var(--text-body-sm);
font-weight: 500;
cursor: default;
}
.ps-lang-code {
font-size: var(--text-label);
}
@media (max-width: 767px) {
.ps-lang-placeholder {
display: none;
}
}
/* ============================================================================
22. FOOTER UPDATES — Disclaimer and language links
============================================================================ */
.footer-disclaimer {
font-size: var(--text-caption);
color: var(--color-text-tertiary);
max-width: var(--container-md);
margin: var(--spacing-md) auto;
line-height: 1.5;
}
.footer-languages {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-md);
margin-top: var(--spacing-md);
font-size: var(--text-body-sm);
flex-wrap: wrap;
}
.footer-languages-label {
font-weight: 500;
color: var(--color-text-secondary);
}
.footer-languages a {
color: var(--color-brand-primary);
}
/* ============================================================================
23. PROFILE PAGE — Basic profile fallback
============================================================================ */
.ps-profile-basic {
text-align: center;
padding: var(--spacing-2xl) 0;
}
.ps-profile-basic h1 {
margin-bottom: var(--spacing-md);
}
.ps-profile-links {
display: flex;
gap: var(--spacing-md);
justify-content: center;
flex-wrap: wrap;
margin-top: var(--spacing-xl);
}
/* Science Feed header */
.ps-science-feed-header {
text-align: center;
margin-bottom: var(--spacing-2xl);
}
.ps-science-feed-header h1 {
margin-bottom: var(--spacing-md);
}
.ps-science-feed-header p {
color: var(--color-text-secondary);
}
/* ============================================================================
24. DARK MODE — Additions for new components
============================================================================ */
html[data-theme="dark"] .ps-module-card__icon {
background-color: rgba(59, 130, 246, 0.15);
}
html[data-theme="dark"] .ps-module-card:hover {
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.15);
}
html[data-theme="dark"] .ps-docs-warning {
background-color: #78350F;
border-color: #F59E0B;
color: #FEF3C7;
}
html[data-theme="dark"] .ps-auth-card {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
html[data-theme="dark"] .ps-newsletter-section {
background-color: var(--color-bg-secondary);
}
html[data-theme="dark"] .ps-settings-overlay {
background: rgba(0, 0, 0, 0.6);
}
html[data-theme="dark"] .ps-modules-section {
background-color: var(--color-bg-secondary);
}
/* Dropdown submenu support for primary navigation */
.primary-navigation li.menu-item-has-children {
position: relative;
}
.primary-navigation .sub-menu {
display: none;
position: absolute;
top: 100%;
left: 0;
background-color: var(--color-bg-primary);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
min-width: 200px;
z-index: var(--z-dropdown);
padding: var(--spacing-xs) 0;
margin-top: var(--spacing-sm);
}
.primary-navigation li.menu-item-has-children:hover > .sub-menu {
display: flex;
flex-direction: column;
}
.primary-navigation .sub-menu li {
display: block;
}
.primary-navigation .sub-menu a {
display: block;
padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--text-body-sm);
white-space: nowrap;
}
.primary-navigation .sub-menu a:hover {
background-color: var(--color-bg-secondary);
}
.primary-navigation .sub-menu a::after {
display: none;
}
/* ============================================================================
25. RESPONSIVE — Additions for new components
============================================================================ */
@media (max-width: 767px) {
.ps-user-menu__toggle {
padding: var(--spacing-xs) var(--spacing-sm);
font-size: var(--text-caption);
}
.ps-user-menu__name {
display: none;
}
.ps-modules-grid {
grid-template-columns: 1fr;
}
}
@media (min-width: 768px) {
.ps-modules-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.ps-modules-grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* ============================================================================
END OF STYLESHEET
============================================================================ */

View File

@@ -0,0 +1,98 @@
<?php
/**
* Template Part: Research Module Cards
*
* Renders a 6-card grid of research modules (tools/sections) for the front page.
* Each card checks if the target plugin is active; inactive cards are dimmed.
*
* @see front-page.php — includes this template part
* @see style.css — .ps-module-* classes
*
* What: 3-column grid of research tool cards with SVG icons.
* Who calls it: get_template_part( 'template-parts/module', 'cards' ).
* Dependencies: None — all output is self-contained.
*
* @package peptide-starter
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Module card definitions with shortcode checks for graceful degradation.
$modules = array(
array(
'title' => __( 'Peptides', 'peptide-starter' ),
'description' => __( 'Browse the complete reagent directory with protocols and research parameters.', 'peptide-starter' ),
'url' => '/peptides',
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L12 22"/><path d="M5 12C5 8.13 8.13 5 12 5s7 3.13 7 7-3.13 7-7 7-7-3.13-7-7z"/><circle cx="12" cy="8" r="1.5" fill="currentColor" stroke="none"/><circle cx="12" cy="16" r="1.5" fill="currentColor" stroke="none"/></svg>',
'available' => shortcode_exists( 'peptide_directory' ) || post_type_exists( 'peptide' ),
),
array(
'title' => __( 'Calculator', 'peptide-starter' ),
'description' => __( 'Precision reconstitution calculator for accurate laboratory preparations.', 'peptide-starter' ),
'url' => '/calculator',
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><line x1="8" y1="10" x2="10" y2="10"/><line x1="14" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="10" y2="14"/><line x1="14" y1="14" x2="16" y2="14"/><line x1="8" y1="18" x2="16" y2="18"/></svg>',
'available' => shortcode_exists( 'peptide_tools_calculator' ),
),
array(
'title' => __( 'Protocol Builder', 'peptide-starter' ),
'description' => __( 'Interactive multi-step wizard for designing research protocols.', 'peptide-starter' ),
'url' => '/protocol-builder',
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2"/><rect x="9" y="3" width="6" height="4" rx="1"/><path d="M9 14l2 2 4-4"/></svg>',
'available' => shortcode_exists( 'peptide_tools_protocol_builder' ),
),
array(
'title' => __( 'Tracker', 'peptide-starter' ),
'description' => __( 'Log doses, manage active compounds, and forecast vial depletion timelines.', 'peptide-starter' ),
'url' => '/tracker',
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/><line x1="3" y1="20" x2="21" y2="20"/></svg>',
'available' => shortcode_exists( 'peptide_tracker' ),
),
array(
'title' => __( 'Science Feed', 'peptide-starter' ),
'description' => __( 'Latest research publications, clinical studies, and laboratory insights.', 'peptide-starter' ),
'url' => '/news',
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 22h16a2 2 0 002-2V4a2 2 0 00-2-2H8a2 2 0 00-2 2v16a2 2 0 01-2 2zm0 0a2 2 0 01-2-2v-9c0-1.1.9-2 2-2h2"/><line x1="10" y1="6" x2="18" y2="6"/><line x1="10" y1="10" x2="18" y2="10"/><line x1="10" y1="14" x2="14" y2="14"/></svg>',
'available' => shortcode_exists( 'peptide_news' ),
),
array(
'title' => __( 'Subject Log', 'peptide-starter' ),
'description' => __( 'Longitudinal tracking for biomarker data and protocol history.', 'peptide-starter' ),
'url' => '/subject-log',
'icon' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
'available' => shortcode_exists( 'peptide_tracker_subject_log' ),
),
);
?>
<section class="ps-modules-section">
<div class="ps-container-wide">
<div class="ps-modules-header">
<h2 class="ps-modules-title"><?php esc_html_e( 'Research Modules', 'peptide-starter' ); ?></h2>
<p class="ps-modules-subtitle">
<?php esc_html_e( 'Tools and resources for laboratory research and data management.', 'peptide-starter' ); ?>
</p>
</div>
<div class="ps-modules-grid">
<?php foreach ( $modules as $module ) : ?>
<a href="<?php echo esc_url( home_url( $module['url'] ) ); ?>"
class="ps-module-card <?php echo $module['available'] ? '' : 'ps-module-card--inactive'; ?>">
<div class="ps-module-card__icon">
<?php
// SVG icons are defined in this template — safe to output directly.
echo $module['icon']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static SVG
?>
</div>
<h3 class="ps-module-card__title"><?php echo esc_html( $module['title'] ); ?></h3>
<p class="ps-module-card__description"><?php echo esc_html( $module['description'] ); ?></p>
<span class="ps-module-card__link">
<?php esc_html_e( 'Open', 'peptide-starter' ); ?> &rarr;
</span>
</a>
<?php endforeach; ?>
</div>
</div>
</section>

View File

@@ -0,0 +1,82 @@
<?php
/**
* Template Part: Newsletter Signup
*
* Reusable newsletter signup section displayed on front page and science feed.
* Stores emails in wp_options as a placeholder until Mailchimp/similar is integrated.
*
* @see functions.php — peptide_starter_handle_newsletter_signup() for form processing
* @see front-page.php — includes this template part
* @see page-science-feed.php — includes this template part
*
* What: Renders the "Monthly Research Dispatch" email signup form.
* Who calls it: get_template_part( 'template-parts/newsletter', 'signup' ).
* Dependencies: None — self-contained HTML form.
*
* @package peptide-starter
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Check for newsletter submission status messages.
$newsletter_status = isset( $_GET['ps_newsletter'] ) ? sanitize_text_field( wp_unslash( $_GET['ps_newsletter'] ) ) : '';
$status_message = '';
$status_class = '';
if ( 'success' === $newsletter_status ) {
$status_message = __( 'Thanks for subscribing! You will receive our next dispatch.', 'peptide-starter' );
$status_class = 'ps-alert-success';
} elseif ( 'invalid' === $newsletter_status ) {
$status_message = __( 'Please enter a valid email address.', 'peptide-starter' );
$status_class = 'ps-alert-error';
} elseif ( 'duplicate' === $newsletter_status ) {
$status_message = __( 'This email is already subscribed.', 'peptide-starter' );
$status_class = 'ps-alert-info';
}
?>
<section class="ps-newsletter-section">
<div class="ps-container">
<div class="ps-newsletter-inner">
<h2 class="ps-newsletter-title">
<?php esc_html_e( 'Monthly Research Dispatch', 'peptide-starter' ); ?>
</h2>
<p class="ps-newsletter-subtitle">
<?php esc_html_e( "First edition drops soon. Don't miss the data.", 'peptide-starter' ); ?>
</p>
<?php if ( $status_message ) : ?>
<div class="ps-alert <?php echo esc_attr( $status_class ); ?> ps-newsletter-status" role="alert">
<p><?php echo esc_html( $status_message ); ?></p>
</div>
<?php endif; ?>
<form class="ps-newsletter-form" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post">
<?php wp_nonce_field( 'peptide_starter_newsletter', 'ps_newsletter_nonce' ); ?>
<input type="hidden" name="action" value="peptide_starter_newsletter_signup">
<div class="ps-newsletter-form-row">
<input
type="email"
name="email"
class="ps-newsletter-input"
placeholder="<?php esc_attr_e( 'your@email.com', 'peptide-starter' ); ?>"
required
aria-label="<?php esc_attr_e( 'Email address', 'peptide-starter' ); ?>"
>
<button type="submit" class="ps-btn ps-btn-primary">
<?php esc_html_e( 'Subscribe', 'peptide-starter' ); ?>
</button>
</div>
</form>
<p class="ps-newsletter-learn-more">
<a href="<?php echo esc_url( home_url( '/documentation' ) ); ?>">
<?php esc_html_e( 'Learn more about the dispatch', 'peptide-starter' ); ?> &rarr;
</a>
</p>
</div>
</div>
</section>

View File

@@ -0,0 +1,102 @@
<?php
/**
* Template Part: Settings Panel
*
* Slide-out panel triggered by the settings icon in the header.
* Contains contact form, compound data submission link, and support info.
*
* @see assets/js/settings-panel.js — open/close toggle
* @see functions.php — AJAX handler for contact form
* @see header.php — settings icon trigger
*
* What: Slide-out support panel with contact form and links.
* Who calls it: get_template_part( 'template-parts/settings', 'panel' ) in footer.php.
* Dependencies: None — self-contained HTML.
*
* @package peptide-starter
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$current_user = wp_get_current_user();
$user_name = is_user_logged_in() ? $current_user->display_name : '';
$user_email = is_user_logged_in() ? $current_user->user_email : '';
?>
<!-- Settings Panel Overlay -->
<div class="ps-settings-overlay" id="ps-settings-overlay" aria-hidden="true"></div>
<!-- Settings Panel -->
<aside class="ps-settings-panel" id="ps-settings-panel" aria-hidden="true" aria-label="<?php esc_attr_e( 'Support panel', 'peptide-starter' ); ?>">
<div class="ps-settings-panel__header">
<h2><?php esc_html_e( 'Support', 'peptide-starter' ); ?></h2>
<button class="ps-settings-panel__close" id="ps-settings-close" aria-label="<?php esc_attr_e( 'Close panel', 'peptide-starter' ); ?>">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M15 5L5 15M5 5L15 15"/>
</svg>
</button>
</div>
<div class="ps-settings-panel__body">
<!-- Contact Engineering Form -->
<div class="ps-settings-section">
<h3><?php esc_html_e( 'Contact Engineering', 'peptide-starter' ); ?></h3>
<form id="ps-contact-form" class="ps-contact-form" novalidate>
<?php wp_nonce_field( 'ps_contact_form', 'ps_contact_nonce' ); ?>
<input type="hidden" name="action" value="ps_contact_submit">
<div class="ps-form-group">
<label for="ps-contact-name"><?php esc_html_e( 'Name', 'peptide-starter' ); ?></label>
<input type="text" id="ps-contact-name" name="name" value="<?php echo esc_attr( $user_name ); ?>" required>
</div>
<div class="ps-form-group">
<label for="ps-contact-email"><?php esc_html_e( 'Email', 'peptide-starter' ); ?></label>
<input type="email" id="ps-contact-email" name="email" value="<?php echo esc_attr( $user_email ); ?>" required>
</div>
<div class="ps-form-group">
<label for="ps-contact-topic"><?php esc_html_e( 'Topic', 'peptide-starter' ); ?></label>
<select id="ps-contact-topic" name="topic" required>
<option value=""><?php esc_html_e( 'Select a topic', 'peptide-starter' ); ?></option>
<option value="bug"><?php esc_html_e( 'Bug Report', 'peptide-starter' ); ?></option>
<option value="feature"><?php esc_html_e( 'Feature Request', 'peptide-starter' ); ?></option>
<option value="data"><?php esc_html_e( 'Data Correction', 'peptide-starter' ); ?></option>
<option value="other"><?php esc_html_e( 'Other', 'peptide-starter' ); ?></option>
</select>
</div>
<div class="ps-form-group">
<label for="ps-contact-message"><?php esc_html_e( 'Description', 'peptide-starter' ); ?></label>
<textarea id="ps-contact-message" name="message" rows="4" required></textarea>
</div>
<button type="submit" class="ps-btn ps-btn-primary ps-btn-full">
<?php esc_html_e( 'Send Message', 'peptide-starter' ); ?>
</button>
<div id="ps-contact-status" class="ps-contact-status" aria-live="polite"></div>
</form>
</div>
<!-- Submit Compound Data -->
<div class="ps-settings-section">
<h3><?php esc_html_e( 'Submit Compound Data', 'peptide-starter' ); ?></h3>
<p><?php esc_html_e( 'Have data on a peptide not in our directory? Submit it for review.', 'peptide-starter' ); ?></p>
<a href="<?php echo esc_url( home_url( '/compound-request' ) ); ?>" class="ps-btn ps-btn-secondary ps-btn-full">
<?php esc_html_e( 'Submit Compound', 'peptide-starter' ); ?>
</a>
</div>
<!-- Support Policy -->
<div class="ps-settings-section ps-settings-disclaimer">
<h3><?php esc_html_e( 'Support Policy', 'peptide-starter' ); ?></h3>
<p>
<?php esc_html_e( 'Peptide Repo is a research reference database only. All data is provided for informational and educational purposes. This platform does not provide medical advice, diagnoses, or treatment recommendations. Always consult qualified professionals for clinical decisions.', 'peptide-starter' ); ?>
</p>
</div>
</div>
</aside>