feat: ship Peptide Reconstitution Calculator v1.0.0
Interactive reconstitution calculator with peptide-specific presets. Core features: - [prc_calculator] shortcode with instant client-side math - Concentration (mg/mL), mcg/unit, injection volume, syringe units, doses/vial - Live presets from PR Core dosing data (graceful fallback to 8 defaults) - REST API (prc/v1): presets list/detail + server-side calculate - Transient-cached presets with event-driven invalidation - Dark mode + responsive design (320px+) - Full teardown in uninstall.php 12 PHP files (max 241 lines), 1 JS file, 1 CSS file. CI: PHP lint 8.1-8.3, PHPCS WordPress, 300-line check. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
167
includes/api/class-prc-rest-controller.php
Normal file
167
includes/api/class-prc-rest-controller.php
Normal file
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API controller for the reconstitution calculator.
|
||||
*
|
||||
* What: Exposes presets and calculation endpoints for the frontend JS.
|
||||
* Who calls it: rest_api_init hook in PRC_Calculator::boot().
|
||||
* Dependencies: PRC_Preset_Provider, PRC_Math.
|
||||
*
|
||||
* @see class-prc-preset-provider.php — Data source for presets.
|
||||
* @see class-prc-math.php — Calculation engine.
|
||||
* @see class-prc-shortcode.php — Frontend JS consumes these endpoints.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* REST endpoints for the reconstitution calculator.
|
||||
*
|
||||
* Namespace: prc/v1
|
||||
* Endpoints:
|
||||
* GET /presets — List all available peptide presets.
|
||||
* GET /presets/{slug} — Get a single preset by slug.
|
||||
* POST /calculate — Run reconstitution math server-side.
|
||||
*/
|
||||
class PRC_Rest_Controller {
|
||||
|
||||
/**
|
||||
* REST namespace.
|
||||
*/
|
||||
private const NAMESPACE = 'prc/v1';
|
||||
|
||||
/**
|
||||
* Register all routes.
|
||||
*
|
||||
* @return void Side effect: registers REST routes.
|
||||
*/
|
||||
public function register_routes(): void {
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/presets',
|
||||
[
|
||||
'methods' => 'GET',
|
||||
'callback' => [ $this, 'get_presets' ],
|
||||
'permission_callback' => '__return_true',
|
||||
]
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/presets/(?P<slug>[a-z0-9-]+)',
|
||||
[
|
||||
'methods' => 'GET',
|
||||
'callback' => [ $this, 'get_preset' ],
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => [
|
||||
'slug' => [
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_title',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/calculate',
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'callback' => [ $this, 'calculate' ],
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => $this->get_calculate_args(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /presets — return all available presets.
|
||||
*
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function get_presets( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$provider = new PRC_Preset_Provider();
|
||||
$presets = $provider->get_all_presets();
|
||||
|
||||
return new \WP_REST_Response( $presets, 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /presets/{slug} — return a single preset.
|
||||
*
|
||||
* @param \WP_REST_Request $request Request with 'slug' param.
|
||||
* @return \WP_REST_Response|\WP_Error
|
||||
*/
|
||||
public function get_preset( \WP_REST_Request $request ) {
|
||||
$slug = $request->get_param( 'slug' );
|
||||
$provider = new PRC_Preset_Provider();
|
||||
$presets = $provider->get_all_presets();
|
||||
|
||||
foreach ( $presets as $preset ) {
|
||||
if ( $preset['slug'] === $slug ) {
|
||||
return new \WP_REST_Response( $preset, 200 );
|
||||
}
|
||||
}
|
||||
|
||||
return new \WP_Error(
|
||||
'prc_preset_not_found',
|
||||
__( 'Preset not found.', 'peptide-reconstitution-calculator' ),
|
||||
[ 'status' => 404 ]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /calculate — run reconstitution math.
|
||||
*
|
||||
* Accepts vial_mg, water_ml, desired_dose, dose_unit and returns
|
||||
* concentration, injection volume, syringe units, and doses per vial.
|
||||
*
|
||||
* @param \WP_REST_Request $request Request with calculation params.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function calculate( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$result = PRC_Math::calculate(
|
||||
(float) $request->get_param( 'vial_mg' ),
|
||||
(float) $request->get_param( 'water_ml' ),
|
||||
(float) $request->get_param( 'desired_dose' ),
|
||||
(string) $request->get_param( 'dose_unit' )
|
||||
);
|
||||
|
||||
return new \WP_REST_Response( $result, 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Define validation args for the /calculate endpoint.
|
||||
*
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
private function get_calculate_args(): array {
|
||||
return [
|
||||
'vial_mg' => [
|
||||
'type' => 'number',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'floatval',
|
||||
'validate_callback' => fn( $v ) => is_numeric( $v ) && (float) $v > 0,
|
||||
],
|
||||
'water_ml' => [
|
||||
'type' => 'number',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'floatval',
|
||||
'validate_callback' => fn( $v ) => is_numeric( $v ) && (float) $v > 0,
|
||||
],
|
||||
'desired_dose' => [
|
||||
'type' => 'number',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'floatval',
|
||||
'validate_callback' => fn( $v ) => is_numeric( $v ) && (float) $v > 0,
|
||||
],
|
||||
'dose_unit' => [
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
'validate_callback' => fn( $v ) => in_array( $v, [ 'mcg', 'mg' ], true ),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
28
includes/class-prc-activator.php
Normal file
28
includes/class-prc-activator.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* Activation handler.
|
||||
*
|
||||
* What: Runs on plugin activation — flushes rewrite rules.
|
||||
* Who calls it: register_activation_hook in bootstrap.
|
||||
* Dependencies: None.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Handles plugin activation tasks.
|
||||
*/
|
||||
class PRC_Activator {
|
||||
|
||||
/**
|
||||
* Run activation tasks.
|
||||
*
|
||||
* Flushes rewrite rules so any new pages using the shortcode
|
||||
* are immediately accessible.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function activate(): void {
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
}
|
||||
67
includes/class-prc-autoloader.php
Normal file
67
includes/class-prc-autoloader.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
/**
|
||||
* SPL autoloader for PRC_-prefixed classes.
|
||||
*
|
||||
* What: Maps PRC_ class names to kebab-case filenames under includes/.
|
||||
* Who calls it: Required once in the bootstrap file (peptide-reconstitution-calculator.php).
|
||||
* Dependencies: None.
|
||||
*
|
||||
* @see class-pr-core-autoloader.php in Peptide Repo Core — same pattern.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Autoloads PRC_ classes from includes/ and its immediate subdirectories.
|
||||
*/
|
||||
class PRC_Autoloader {
|
||||
|
||||
/**
|
||||
* Register the autoloader with SPL.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register(): void {
|
||||
spl_autoload_register( [ __CLASS__, 'autoload' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Autoload callback — resolve PRC_ class name to file path.
|
||||
*
|
||||
* Strips PRC_ prefix, converts underscores to hyphens, lowercases,
|
||||
* prepends "class-prc-", and searches includes/ + subdirs.
|
||||
*
|
||||
* @param string $class_name Fully qualified class name.
|
||||
* @return void
|
||||
*/
|
||||
public static function autoload( string $class_name ): void {
|
||||
if ( 0 !== strpos( $class_name, 'PRC_' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$relative = substr( $class_name, 4 ); // Strip "PRC_".
|
||||
$filename = 'class-prc-' . str_replace( '_', '-', strtolower( $relative ) ) . '.php';
|
||||
|
||||
$base = PRC_PLUGIN_DIR . 'includes/';
|
||||
|
||||
// Check includes/ root first.
|
||||
if ( file_exists( $base . $filename ) ) {
|
||||
require_once $base . $filename;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check immediate subdirectories.
|
||||
$dirs = glob( $base . '*', GLOB_ONLYDIR );
|
||||
if ( ! is_array( $dirs ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $dirs as $dir ) {
|
||||
$path = $dir . '/' . $filename;
|
||||
if ( file_exists( $path ) ) {
|
||||
require_once $path;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
47
includes/class-prc-cache-listener.php
Normal file
47
includes/class-prc-cache-listener.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/**
|
||||
* Cache invalidation listener — keeps presets in sync with PR Core.
|
||||
*
|
||||
* What: Listens for PR Core dosing row lifecycle hooks and clears
|
||||
* the preset cache so the calculator picks up fresh data.
|
||||
* Who calls it: PRC_Calculator::boot() registers hooks on init.
|
||||
* Dependencies: PRC_Preset_Provider (cache invalidation method).
|
||||
*
|
||||
* @see class-prc-preset-provider.php — Cache being invalidated.
|
||||
* @see PR_Core_Dosing_Repository — Fires the hooks we listen to.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Hooks into PR Core actions to invalidate the presets cache.
|
||||
*/
|
||||
class PRC_Cache_Listener {
|
||||
|
||||
/**
|
||||
* Register PR Core hooks for cache invalidation.
|
||||
*
|
||||
* Safe to call even when PR Core is not active — the hooks
|
||||
* simply never fire.
|
||||
*
|
||||
* @return void Side effect: registers action hooks.
|
||||
*/
|
||||
public static function register(): void {
|
||||
$callback = [ __CLASS__, 'invalidate' ];
|
||||
|
||||
// Dosing row lifecycle — affects calculator presets.
|
||||
add_action( 'pr_core_after_dosing_row_publish', $callback );
|
||||
|
||||
// Peptide lifecycle — new peptides mean new preset candidates.
|
||||
add_action( 'pr_core_after_peptide_publish', $callback );
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the preset cache.
|
||||
*
|
||||
* @return void Side effect: deletes transient.
|
||||
*/
|
||||
public static function invalidate(): void {
|
||||
( new PRC_Preset_Provider() )->invalidate_cache();
|
||||
}
|
||||
}
|
||||
86
includes/class-prc-calculator.php
Normal file
86
includes/class-prc-calculator.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
/**
|
||||
* Main orchestrator for the Reconstitution Calculator plugin.
|
||||
*
|
||||
* What: Boots all subsystems — shortcode, REST API, admin settings.
|
||||
* Who calls it: plugins_loaded hook in bootstrap.
|
||||
* Dependencies: PRC_Shortcode, PRC_Rest_Controller, PRC_Preset_Provider.
|
||||
*
|
||||
* @see class-prc-shortcode.php — Frontend calculator widget.
|
||||
* @see class-prc-rest-controller.php — REST endpoints for presets.
|
||||
* @see class-prc-preset-provider.php — PR Core integration + fallback defaults.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Central boot class — wires hooks for the entire plugin.
|
||||
*/
|
||||
class PRC_Calculator {
|
||||
|
||||
/**
|
||||
* Boot the plugin — called on plugins_loaded.
|
||||
*
|
||||
* Registers the shortcode, REST routes, and admin notice
|
||||
* if PR Core is not active.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function boot(): void {
|
||||
// Frontend shortcode — always available.
|
||||
$shortcode = new PRC_Shortcode();
|
||||
add_action( 'init', [ $shortcode, 'register' ] );
|
||||
|
||||
// REST API — serves presets to the JS calculator.
|
||||
add_action( 'rest_api_init', [ new PRC_Rest_Controller(), 'register_routes' ] );
|
||||
|
||||
// Cache invalidation — keep presets in sync with PR Core dosing data.
|
||||
PRC_Cache_Listener::register();
|
||||
|
||||
// Admin notice when PR Core is absent.
|
||||
if ( is_admin() && ! self::is_pr_core_active() ) {
|
||||
add_action( 'admin_notices', [ __CLASS__, 'render_pr_core_notice' ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether Peptide Repo Core is active.
|
||||
*
|
||||
* Looks for the PR_CORE_VERSION constant which is defined
|
||||
* in peptide-repo-core.php's bootstrap.
|
||||
*
|
||||
* @return bool True if PR Core is loaded.
|
||||
*/
|
||||
public static function is_pr_core_active(): bool {
|
||||
return defined( 'PR_CORE_VERSION' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an admin notice suggesting PR Core installation.
|
||||
*
|
||||
* Not a blocker — the calculator works standalone with built-in
|
||||
* defaults. PR Core just unlocks peptide-specific presets.
|
||||
*
|
||||
* @return void Side effect: outputs HTML notice.
|
||||
*/
|
||||
public static function render_pr_core_notice(): void {
|
||||
$screen = get_current_screen();
|
||||
if ( null === $screen ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only show on plugins page and the calculator's own pages.
|
||||
$show_on = [ 'plugins', 'settings_page_prc-settings' ];
|
||||
if ( ! in_array( $screen->id, $show_on, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
printf(
|
||||
'<div class="notice notice-info is-dismissible"><p>%s</p></div>',
|
||||
esc_html__(
|
||||
'Peptide Reconstitution Calculator: Install Peptide Repo Core to unlock peptide-specific presets with real dosing data. The calculator works without it using built-in defaults.',
|
||||
'peptide-reconstitution-calculator'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
27
includes/class-prc-deactivator.php
Normal file
27
includes/class-prc-deactivator.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* Deactivation handler.
|
||||
*
|
||||
* What: Runs on plugin deactivation — flushes rewrite rules.
|
||||
* Who calls it: register_deactivation_hook in bootstrap.
|
||||
* Dependencies: None.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Handles plugin deactivation tasks.
|
||||
*/
|
||||
class PRC_Deactivator {
|
||||
|
||||
/**
|
||||
* Run deactivation tasks.
|
||||
*
|
||||
* Flushes rewrite rules to remove any routes this plugin registered.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function deactivate(): void {
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
}
|
||||
145
includes/class-prc-default-presets.php
Normal file
145
includes/class-prc-default-presets.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
/**
|
||||
* Hardcoded default presets — fallback when PR Core is not active.
|
||||
*
|
||||
* What: Provides a curated list of popular peptide presets so the
|
||||
* calculator is useful out of the box, even without the data layer.
|
||||
* Who calls it: PRC_Preset_Provider when PR Core is absent or as gap-fill.
|
||||
* Dependencies: None.
|
||||
*
|
||||
* @see class-prc-preset-provider.php — Merges these with live PR Core data.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Static class containing built-in peptide presets.
|
||||
*/
|
||||
class PRC_Default_Presets {
|
||||
|
||||
/**
|
||||
* Get all built-in presets.
|
||||
*
|
||||
* Each preset includes common vial sizes, recommended reconstitution
|
||||
* volume, typical dose ranges, and frequency. Data sourced from
|
||||
* published literature and commonly available vendor specifications.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public static function get_all(): array {
|
||||
return [
|
||||
[
|
||||
'slug' => 'bpc-157',
|
||||
'name' => 'BPC-157',
|
||||
'vial_sizes_mg' => [ 5, 10 ],
|
||||
'default_vial_mg' => 5,
|
||||
'recommended_water_ml' => 2.0,
|
||||
'dose_range_min' => 200,
|
||||
'dose_range_max' => 500,
|
||||
'dose_unit' => 'mcg',
|
||||
'typical_frequency' => 'twice daily',
|
||||
'evidence_strength' => 'observational',
|
||||
'dosing_row_count' => 0,
|
||||
'source' => 'default',
|
||||
],
|
||||
[
|
||||
'slug' => 'tb-500',
|
||||
'name' => 'TB-500',
|
||||
'vial_sizes_mg' => [ 2, 5, 10 ],
|
||||
'default_vial_mg' => 5,
|
||||
'recommended_water_ml' => 2.0,
|
||||
'dose_range_min' => 2000,
|
||||
'dose_range_max' => 5000,
|
||||
'dose_unit' => 'mcg',
|
||||
'typical_frequency' => 'twice weekly',
|
||||
'evidence_strength' => 'preclinical',
|
||||
'dosing_row_count' => 0,
|
||||
'source' => 'default',
|
||||
],
|
||||
[
|
||||
'slug' => 'semaglutide',
|
||||
'name' => 'Semaglutide',
|
||||
'vial_sizes_mg' => [ 2, 5, 10 ],
|
||||
'default_vial_mg' => 5,
|
||||
'recommended_water_ml' => 2.0,
|
||||
'dose_range_min' => 0.25,
|
||||
'dose_range_max' => 2.4,
|
||||
'dose_unit' => 'mg',
|
||||
'typical_frequency' => 'weekly',
|
||||
'evidence_strength' => 'meta-analysis',
|
||||
'dosing_row_count' => 0,
|
||||
'source' => 'default',
|
||||
],
|
||||
[
|
||||
'slug' => 'cjc-1295-dac',
|
||||
'name' => 'CJC-1295 (DAC)',
|
||||
'vial_sizes_mg' => [ 2, 5 ],
|
||||
'default_vial_mg' => 2,
|
||||
'recommended_water_ml' => 1.0,
|
||||
'dose_range_min' => 1000,
|
||||
'dose_range_max' => 2000,
|
||||
'dose_unit' => 'mcg',
|
||||
'typical_frequency' => 'weekly',
|
||||
'evidence_strength' => 'preclinical',
|
||||
'dosing_row_count' => 0,
|
||||
'source' => 'default',
|
||||
],
|
||||
[
|
||||
'slug' => 'ipamorelin',
|
||||
'name' => 'Ipamorelin',
|
||||
'vial_sizes_mg' => [ 2, 5 ],
|
||||
'default_vial_mg' => 5,
|
||||
'recommended_water_ml' => 2.0,
|
||||
'dose_range_min' => 100,
|
||||
'dose_range_max' => 300,
|
||||
'dose_unit' => 'mcg',
|
||||
'typical_frequency' => 'three times daily',
|
||||
'evidence_strength' => 'preclinical',
|
||||
'dosing_row_count' => 0,
|
||||
'source' => 'default',
|
||||
],
|
||||
[
|
||||
'slug' => 'pt-141',
|
||||
'name' => 'PT-141 (Bremelanotide)',
|
||||
'vial_sizes_mg' => [ 2, 10 ],
|
||||
'default_vial_mg' => 10,
|
||||
'recommended_water_ml' => 2.0,
|
||||
'dose_range_min' => 500,
|
||||
'dose_range_max' => 2000,
|
||||
'dose_unit' => 'mcg',
|
||||
'typical_frequency' => 'as needed',
|
||||
'evidence_strength' => 'rct-large',
|
||||
'dosing_row_count' => 0,
|
||||
'source' => 'default',
|
||||
],
|
||||
[
|
||||
'slug' => 'mk-677',
|
||||
'name' => 'MK-677 (Ibutamoren)',
|
||||
'vial_sizes_mg' => [ 15, 30 ],
|
||||
'default_vial_mg' => 30,
|
||||
'recommended_water_ml' => 3.0,
|
||||
'dose_range_min' => 10,
|
||||
'dose_range_max' => 25,
|
||||
'dose_unit' => 'mg',
|
||||
'typical_frequency' => 'daily',
|
||||
'evidence_strength' => 'rct-small',
|
||||
'dosing_row_count' => 0,
|
||||
'source' => 'default',
|
||||
],
|
||||
[
|
||||
'slug' => 'ghrp-6',
|
||||
'name' => 'GHRP-6',
|
||||
'vial_sizes_mg' => [ 5, 10 ],
|
||||
'default_vial_mg' => 5,
|
||||
'recommended_water_ml' => 2.0,
|
||||
'dose_range_min' => 100,
|
||||
'dose_range_max' => 300,
|
||||
'dose_unit' => 'mcg',
|
||||
'typical_frequency' => 'three times daily',
|
||||
'evidence_strength' => 'preclinical',
|
||||
'dosing_row_count' => 0,
|
||||
'source' => 'default',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
146
includes/class-prc-math.php
Normal file
146
includes/class-prc-math.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
/**
|
||||
* Reconstitution math engine.
|
||||
*
|
||||
* What: Pure calculation functions — no side effects, no database access.
|
||||
* Computes concentration, injection volume, syringe units, and
|
||||
* doses per vial from user inputs.
|
||||
* Who calls it: PRC_Rest_Controller (server-side), also mirrored in JS.
|
||||
* Dependencies: None.
|
||||
*
|
||||
* @see assets/js/calculator.js — Client-side mirror of this math.
|
||||
* @see api/class-prc-rest-controller.php — Server-side consumer.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Static math functions for reconstitution calculations.
|
||||
*
|
||||
* All methods are pure — same inputs always produce same outputs.
|
||||
* The JS calculator mirrors this logic client-side for instant feedback;
|
||||
* the REST endpoint provides server-side validation.
|
||||
*/
|
||||
class PRC_Math {
|
||||
|
||||
/**
|
||||
* Standard insulin syringe size in units (100 IU = 1 mL).
|
||||
*/
|
||||
private const SYRINGE_UNITS_PER_ML = 100;
|
||||
|
||||
/**
|
||||
* Run the full reconstitution calculation.
|
||||
*
|
||||
* @param float $vial_mg Amount of peptide in the vial (mg).
|
||||
* @param float $water_ml Bacteriostatic water added (mL).
|
||||
* @param float $desired_dose Desired dose per injection.
|
||||
* @param string $dose_unit Unit of desired_dose: 'mcg' or 'mg'.
|
||||
* @return array<string, mixed> Calculation results.
|
||||
*/
|
||||
public static function calculate(
|
||||
float $vial_mg,
|
||||
float $water_ml,
|
||||
float $desired_dose,
|
||||
string $dose_unit
|
||||
): array {
|
||||
// Normalize desired dose to mg for consistent math.
|
||||
$dose_mg = ( 'mcg' === $dose_unit )
|
||||
? $desired_dose / 1000
|
||||
: $desired_dose;
|
||||
|
||||
$concentration_mg_per_ml = self::concentration_mg_per_ml( $vial_mg, $water_ml );
|
||||
$concentration_mcg_per_unit = self::concentration_mcg_per_unit( $vial_mg, $water_ml );
|
||||
$injection_ml = self::injection_volume_ml( $dose_mg, $concentration_mg_per_ml );
|
||||
$syringe_units = self::syringe_units( $injection_ml );
|
||||
$doses_per_vial = self::doses_per_vial( $vial_mg, $dose_mg );
|
||||
|
||||
return [
|
||||
'concentration_mg_per_ml' => round( $concentration_mg_per_ml, 4 ),
|
||||
'concentration_mcg_per_unit' => round( $concentration_mcg_per_unit, 2 ),
|
||||
'injection_volume_ml' => round( $injection_ml, 4 ),
|
||||
'syringe_units' => round( $syringe_units, 1 ),
|
||||
'doses_per_vial' => floor( $doses_per_vial ),
|
||||
'total_peptide_mg' => $vial_mg,
|
||||
'water_ml' => $water_ml,
|
||||
'desired_dose_mg' => round( $dose_mg, 4 ),
|
||||
'desired_dose_display' => $desired_dose,
|
||||
'dose_unit' => $dose_unit,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Concentration after reconstitution in mg/mL.
|
||||
*
|
||||
* @param float $vial_mg Peptide amount (mg).
|
||||
* @param float $water_ml Water volume (mL).
|
||||
* @return float mg per mL.
|
||||
*/
|
||||
public static function concentration_mg_per_ml( float $vial_mg, float $water_ml ): float {
|
||||
if ( $water_ml <= 0 ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $vial_mg / $water_ml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concentration in mcg per insulin syringe unit.
|
||||
*
|
||||
* Why: Most users measure with insulin syringes (100 units = 1 mL).
|
||||
* Knowing mcg-per-unit makes it easy to count tick marks.
|
||||
*
|
||||
* @param float $vial_mg Peptide amount (mg).
|
||||
* @param float $water_ml Water volume (mL).
|
||||
* @return float mcg per syringe unit.
|
||||
*/
|
||||
public static function concentration_mcg_per_unit( float $vial_mg, float $water_ml ): float {
|
||||
if ( $water_ml <= 0 ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$total_mcg = $vial_mg * 1000;
|
||||
$total_units = $water_ml * self::SYRINGE_UNITS_PER_ML;
|
||||
|
||||
return $total_mcg / $total_units;
|
||||
}
|
||||
|
||||
/**
|
||||
* Volume to inject for a desired dose (mL).
|
||||
*
|
||||
* @param float $dose_mg Desired dose in mg.
|
||||
* @param float $concentration_mg_ml Solution concentration in mg/mL.
|
||||
* @return float Injection volume in mL.
|
||||
*/
|
||||
public static function injection_volume_ml( float $dose_mg, float $concentration_mg_ml ): float {
|
||||
if ( $concentration_mg_ml <= 0 ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $dose_mg / $concentration_mg_ml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert mL to insulin syringe units.
|
||||
*
|
||||
* @param float $ml Volume in mL.
|
||||
* @return float Syringe units (100 units = 1 mL).
|
||||
*/
|
||||
public static function syringe_units( float $ml ): float {
|
||||
return $ml * self::SYRINGE_UNITS_PER_ML;
|
||||
}
|
||||
|
||||
/**
|
||||
* How many full doses fit in one vial.
|
||||
*
|
||||
* @param float $vial_mg Total peptide in vial (mg).
|
||||
* @param float $dose_mg Dose per injection (mg).
|
||||
* @return float Number of doses (floor to get whole doses).
|
||||
*/
|
||||
public static function doses_per_vial( float $vial_mg, float $dose_mg ): float {
|
||||
if ( $dose_mg <= 0 ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $vial_mg / $dose_mg;
|
||||
}
|
||||
}
|
||||
241
includes/class-prc-preset-provider.php
Normal file
241
includes/class-prc-preset-provider.php
Normal file
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
/**
|
||||
* Preset provider — bridges PR Core dosing data to the calculator.
|
||||
*
|
||||
* What: Builds peptide-specific reconstitution presets from PR Core's
|
||||
* repository classes. Falls back to built-in defaults when PR Core
|
||||
* is not active.
|
||||
* Who calls it: PRC_Rest_Controller serves these to the JS calculator.
|
||||
* Dependencies: PR Core repositories (optional), PRC_Calculator::is_pr_core_active().
|
||||
*
|
||||
* @see class-prc-rest-controller.php — Serves presets via REST.
|
||||
* @see class-prc-default-presets.php — Hardcoded fallback presets.
|
||||
* @see PR_Core_Peptide_Repository (PR Core) — Peptide list source.
|
||||
* @see PR_Core_Dosing_Repository (PR Core) — Dosing data source.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Assembles reconstitution presets — live data from PR Core or static defaults.
|
||||
*/
|
||||
class PRC_Preset_Provider {
|
||||
|
||||
/**
|
||||
* Cache key prefix for transient storage.
|
||||
*/
|
||||
private const CACHE_PREFIX = 'prc_presets_';
|
||||
|
||||
/**
|
||||
* How long to cache presets (seconds). 1 hour.
|
||||
*/
|
||||
private const CACHE_TTL_SECONDS = 3600;
|
||||
|
||||
/**
|
||||
* Get all available presets for the calculator dropdown.
|
||||
*
|
||||
* Returns a merged list: PR Core live data (if active) + built-in defaults
|
||||
* for peptides not yet in the database.
|
||||
*
|
||||
* @return array<int, array<string, mixed>> Array of preset objects.
|
||||
*/
|
||||
public function get_all_presets(): array {
|
||||
if ( ! PRC_Calculator::is_pr_core_active() ) {
|
||||
return PRC_Default_Presets::get_all();
|
||||
}
|
||||
|
||||
$cached = get_transient( self::CACHE_PREFIX . 'all' );
|
||||
if ( false !== $cached && is_array( $cached ) ) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$presets = $this->build_presets_from_pr_core();
|
||||
|
||||
// Merge any default presets for peptides not covered by PR Core.
|
||||
$live_slugs = array_column( $presets, 'slug' );
|
||||
foreach ( PRC_Default_Presets::get_all() as $default ) {
|
||||
if ( ! in_array( $default['slug'], $live_slugs, true ) ) {
|
||||
$presets[] = $default;
|
||||
}
|
||||
}
|
||||
|
||||
set_transient( self::CACHE_PREFIX . 'all', $presets, self::CACHE_TTL_SECONDS );
|
||||
|
||||
return $presets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the presets cache.
|
||||
*
|
||||
* Called when PR Core dosing data changes so the calculator
|
||||
* picks up updated values.
|
||||
*
|
||||
* @return void Side effect: deletes transient.
|
||||
*/
|
||||
public function invalidate_cache(): void {
|
||||
delete_transient( self::CACHE_PREFIX . 'all' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Build preset entries from PR Core's live peptide + dosing data.
|
||||
*
|
||||
* For each published peptide with subcutaneous dosing rows, extracts
|
||||
* common vial sizes, dose range, and typical reconstitution volume.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function build_presets_from_pr_core(): array {
|
||||
$peptide_repo = new PR_Core_Peptide_Repository();
|
||||
$dosing_repo = new PR_Core_Dosing_Repository();
|
||||
|
||||
$peptides = $peptide_repo->find_all( [ 'posts_per_page' => 100 ] );
|
||||
$presets = [];
|
||||
|
||||
foreach ( $peptides as $peptide ) {
|
||||
$rows = $dosing_repo->find_by_peptide(
|
||||
$peptide->id,
|
||||
[ 'route' => 'subcutaneous' ]
|
||||
);
|
||||
|
||||
if ( empty( $rows ) ) {
|
||||
// No subcutaneous dosing data — skip this peptide.
|
||||
continue;
|
||||
}
|
||||
|
||||
$preset = $this->build_single_preset( $peptide, $rows );
|
||||
if ( null !== $preset ) {
|
||||
$presets[] = $preset;
|
||||
}
|
||||
}
|
||||
|
||||
return $presets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a single preset from a peptide DTO and its dosing rows.
|
||||
*
|
||||
* @param object $peptide PR_Core_Peptide_DTO.
|
||||
* @param PR_Core_Dosing_Row_DTO[]|null $rows Active subcutaneous dosing rows.
|
||||
* @return array<string, mixed>|null Null if data is insufficient.
|
||||
*/
|
||||
private function build_single_preset( object $peptide, ?array $rows ): ?array {
|
||||
if ( empty( $rows ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract dose range across all rows.
|
||||
$dose_mins = array_filter( array_map( fn( $r ) => $r->dose_min, $rows ) );
|
||||
$dose_maxs = array_filter( array_map( fn( $r ) => $r->dose_max, $rows ) );
|
||||
$dose_unit = $rows[0]->dose_unit ?: 'mcg';
|
||||
|
||||
// Derive typical vial sizes from dose range.
|
||||
// Why: Most peptides are sold in standard vial sizes (2mg, 5mg, 10mg, 15mg, 30mg).
|
||||
$vial_sizes = $this->infer_vial_sizes( $dose_mins, $dose_maxs, $dose_unit );
|
||||
|
||||
$display_name = '';
|
||||
if ( method_exists( $peptide, 'to_array' ) ) {
|
||||
$arr = $peptide->to_array();
|
||||
$display_name = $arr['display_name'] ?? $peptide->title ?? '';
|
||||
} else {
|
||||
$display_name = $peptide->title ?? '';
|
||||
}
|
||||
|
||||
return [
|
||||
'slug' => $peptide->slug,
|
||||
'name' => $display_name ?: $peptide->title,
|
||||
'vial_sizes_mg' => $vial_sizes,
|
||||
'default_vial_mg' => $vial_sizes[0] ?? 5,
|
||||
'recommended_water_ml' => $this->recommend_water_ml( $vial_sizes[0] ?? 5 ),
|
||||
'dose_range_min' => ! empty( $dose_mins ) ? min( $dose_mins ) : null,
|
||||
'dose_range_max' => ! empty( $dose_maxs ) ? max( $dose_maxs ) : null,
|
||||
'dose_unit' => $dose_unit,
|
||||
'typical_frequency' => $this->most_common_frequency( $rows ),
|
||||
'evidence_strength' => $peptide->evidence_strength ?? 'preclinical',
|
||||
'dosing_row_count' => count( $rows ),
|
||||
'source' => 'pr_core',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer standard vial sizes from the dose range.
|
||||
*
|
||||
* Why: Peptide vendors use standard sizes. If a peptide's typical dose
|
||||
* is 250mcg, it's usually sold in 5mg or 10mg vials. This heuristic
|
||||
* picks the 2-3 most likely sizes.
|
||||
*
|
||||
* @param float[] $dose_mins Minimum doses from all rows.
|
||||
* @param float[] $dose_maxs Maximum doses from all rows.
|
||||
* @param string $dose_unit Unit (mcg or mg).
|
||||
* @return float[] Standard vial sizes in mg.
|
||||
*/
|
||||
private function infer_vial_sizes( array $dose_mins, array $dose_maxs, string $dose_unit ): array {
|
||||
$standard_sizes = [ 2, 5, 10, 15, 30 ];
|
||||
|
||||
if ( empty( $dose_mins ) && empty( $dose_maxs ) ) {
|
||||
return [ 5, 10 ];
|
||||
}
|
||||
|
||||
// Convert everything to mg for comparison.
|
||||
$max_dose_mg = 0;
|
||||
$all_doses = array_merge( $dose_mins, $dose_maxs );
|
||||
|
||||
foreach ( $all_doses as $dose ) {
|
||||
$mg = ( 'mcg' === $dose_unit ) ? $dose / 1000 : $dose;
|
||||
$max_dose_mg = max( $max_dose_mg, $mg );
|
||||
}
|
||||
|
||||
// Pick vials that hold at least 10 doses at the max dose level.
|
||||
$viable = [];
|
||||
foreach ( $standard_sizes as $size ) {
|
||||
if ( $max_dose_mg > 0 && ( $size / $max_dose_mg ) >= 5 ) {
|
||||
$viable[] = $size;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: return two middle sizes if heuristic found nothing.
|
||||
return ! empty( $viable ) ? array_slice( $viable, 0, 3 ) : [ 5, 10 ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Recommend reconstitution water volume based on vial size.
|
||||
*
|
||||
* Why: Standard practice is 1-2mL per 5mg, producing round concentrations.
|
||||
*
|
||||
* @param float $vial_mg Vial size in mg.
|
||||
* @return float Recommended bacteriostatic water in mL.
|
||||
*/
|
||||
private function recommend_water_ml( float $vial_mg ): float {
|
||||
if ( $vial_mg <= 2 ) {
|
||||
return 1.0;
|
||||
}
|
||||
if ( $vial_mg <= 5 ) {
|
||||
return 2.0;
|
||||
}
|
||||
if ( $vial_mg <= 10 ) {
|
||||
return 2.0;
|
||||
}
|
||||
if ( $vial_mg <= 15 ) {
|
||||
return 3.0;
|
||||
}
|
||||
|
||||
return 5.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the most common frequency value across dosing rows.
|
||||
*
|
||||
* @param PR_Core_Dosing_Row_DTO[] $rows Dosing rows.
|
||||
* @return string|null Most common frequency, or null if none.
|
||||
*/
|
||||
private function most_common_frequency( array $rows ): ?string {
|
||||
$freqs = array_filter( array_map( fn( $r ) => $r->frequency, $rows ) );
|
||||
if ( empty( $freqs ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$counts = array_count_values( $freqs );
|
||||
arsort( $counts );
|
||||
|
||||
return array_key_first( $counts );
|
||||
}
|
||||
}
|
||||
241
includes/frontend/class-prc-shortcode.php
Normal file
241
includes/frontend/class-prc-shortcode.php
Normal file
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
/**
|
||||
* Shortcode registration and frontend asset enqueueing.
|
||||
*
|
||||
* What: Registers [prc_calculator] shortcode, enqueues JS/CSS,
|
||||
* and renders the calculator HTML shell.
|
||||
* Who calls it: PRC_Calculator::boot() registers this on init.
|
||||
* Dependencies: PRC_Preset_Provider (for inline preset data).
|
||||
*
|
||||
* @see assets/js/calculator.js — Client-side calculator logic.
|
||||
* @see assets/css/calculator.css — Calculator styles.
|
||||
* @see class-prc-calculator.php — Boot orchestrator.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Handles the [prc_calculator] shortcode.
|
||||
*/
|
||||
class PRC_Shortcode {
|
||||
|
||||
/**
|
||||
* Whether assets have been enqueued this request.
|
||||
*
|
||||
* Prevents double-enqueue if shortcode appears multiple times.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private bool $assets_enqueued = false;
|
||||
|
||||
/**
|
||||
* Register the shortcode with WordPress.
|
||||
*
|
||||
* @return void Side effect: registers shortcode.
|
||||
*/
|
||||
public function register(): void {
|
||||
add_shortcode( 'prc_calculator', [ $this, 'render' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the calculator widget.
|
||||
*
|
||||
* Outputs the HTML structure and enqueues JS/CSS on first call.
|
||||
* The JS calculator takes over from here — all interactivity
|
||||
* is client-side for instant feedback.
|
||||
*
|
||||
* @param array<string, string>|string $atts Shortcode attributes.
|
||||
* @return string HTML output.
|
||||
*/
|
||||
public function render( $atts = [] ): string {
|
||||
$atts = shortcode_atts(
|
||||
[
|
||||
'peptide' => '', // Pre-select a peptide by slug.
|
||||
],
|
||||
$atts,
|
||||
'prc_calculator'
|
||||
);
|
||||
|
||||
$this->enqueue_assets();
|
||||
|
||||
$selected_slug = sanitize_title( $atts['peptide'] );
|
||||
|
||||
ob_start();
|
||||
$this->render_calculator_html( $selected_slug );
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue calculator JS and CSS, with inline preset data.
|
||||
*
|
||||
* Why inline presets: Avoids a REST round-trip on page load.
|
||||
* The presets are small (< 5KB) and cached by the provider.
|
||||
*
|
||||
* @return void Side effect: enqueues scripts/styles.
|
||||
*/
|
||||
private function enqueue_assets(): void {
|
||||
if ( $this->assets_enqueued ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style(
|
||||
'prc-calculator',
|
||||
PRC_PLUGIN_URL . 'assets/css/calculator.css',
|
||||
[],
|
||||
PRC_VERSION
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'prc-calculator',
|
||||
PRC_PLUGIN_URL . 'assets/js/calculator.js',
|
||||
[],
|
||||
PRC_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
// Pass presets and config to JS.
|
||||
$provider = new PRC_Preset_Provider();
|
||||
wp_localize_script( 'prc-calculator', 'prcConfig', [
|
||||
'presets' => $provider->get_all_presets(),
|
||||
'restUrl' => esc_url_raw( rest_url( 'prc/v1' ) ),
|
||||
'nonce' => wp_create_nonce( 'wp_rest' ),
|
||||
'syringeUnits' => 100,
|
||||
'i18n' => [
|
||||
'selectPeptide' => __( 'Select a peptide…', 'peptide-reconstitution-calculator' ),
|
||||
'customEntry' => __( 'Custom / Manual Entry', 'peptide-reconstitution-calculator' ),
|
||||
'calculate' => __( 'Calculate', 'peptide-reconstitution-calculator' ),
|
||||
'reset' => __( 'Reset', 'peptide-reconstitution-calculator' ),
|
||||
'concentration' => __( 'Concentration', 'peptide-reconstitution-calculator' ),
|
||||
'injectionVol' => __( 'Injection Volume', 'peptide-reconstitution-calculator' ),
|
||||
'syringeUnits' => __( 'Syringe Units', 'peptide-reconstitution-calculator' ),
|
||||
'dosesPerVial' => __( 'Doses Per Vial', 'peptide-reconstitution-calculator' ),
|
||||
'perUnit' => __( 'per unit', 'peptide-reconstitution-calculator' ),
|
||||
'disclaimer' => __( 'For informational purposes only. Not medical advice. Always consult a qualified healthcare professional.', 'peptide-reconstitution-calculator' ),
|
||||
],
|
||||
] );
|
||||
|
||||
$this->assets_enqueued = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the calculator HTML shell.
|
||||
*
|
||||
* The JS calculator populates the interactive elements.
|
||||
* This provides the semantic structure and no-JS fallback message.
|
||||
*
|
||||
* @param string $selected_slug Pre-selected peptide slug (empty = none).
|
||||
* @return void Side effect: outputs HTML.
|
||||
*/
|
||||
private function render_calculator_html( string $selected_slug ): void {
|
||||
?>
|
||||
<div class="prc-calculator" data-selected-slug="<?php echo esc_attr( $selected_slug ); ?>">
|
||||
<div class="prc-calculator__header">
|
||||
<h3 class="prc-calculator__title">
|
||||
<?php esc_html_e( 'Peptide Reconstitution Calculator', 'peptide-reconstitution-calculator' ); ?>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="prc-calculator__body">
|
||||
<!-- Peptide selector -->
|
||||
<div class="prc-field">
|
||||
<label class="prc-field__label" for="prc-peptide-select">
|
||||
<?php esc_html_e( 'Peptide', 'peptide-reconstitution-calculator' ); ?>
|
||||
</label>
|
||||
<select id="prc-peptide-select" class="prc-field__input">
|
||||
<option value=""><?php esc_html_e( 'Select a peptide…', 'peptide-reconstitution-calculator' ); ?></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Vial size -->
|
||||
<div class="prc-field">
|
||||
<label class="prc-field__label" for="prc-vial-mg">
|
||||
<?php esc_html_e( 'Vial Size', 'peptide-reconstitution-calculator' ); ?>
|
||||
</label>
|
||||
<div class="prc-field__row">
|
||||
<select id="prc-vial-select" class="prc-field__input prc-field__input--half"></select>
|
||||
<div class="prc-field__custom-wrap">
|
||||
<input type="number" id="prc-vial-mg" class="prc-field__input prc-field__input--half"
|
||||
step="0.1" min="0.1" placeholder="5" />
|
||||
<span class="prc-field__unit"><?php esc_html_e( 'mg', 'peptide-reconstitution-calculator' ); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bacteriostatic water -->
|
||||
<div class="prc-field">
|
||||
<label class="prc-field__label" for="prc-water-ml">
|
||||
<?php esc_html_e( 'Bacteriostatic Water', 'peptide-reconstitution-calculator' ); ?>
|
||||
</label>
|
||||
<div class="prc-field__row">
|
||||
<input type="number" id="prc-water-ml" class="prc-field__input"
|
||||
step="0.1" min="0.1" placeholder="2.0" />
|
||||
<span class="prc-field__unit"><?php esc_html_e( 'mL', 'peptide-reconstitution-calculator' ); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desired dose -->
|
||||
<div class="prc-field">
|
||||
<label class="prc-field__label" for="prc-desired-dose">
|
||||
<?php esc_html_e( 'Desired Dose', 'peptide-reconstitution-calculator' ); ?>
|
||||
</label>
|
||||
<div class="prc-field__row">
|
||||
<input type="number" id="prc-desired-dose" class="prc-field__input"
|
||||
step="any" min="0" placeholder="250" />
|
||||
<select id="prc-dose-unit" class="prc-field__input prc-field__input--unit">
|
||||
<option value="mcg">mcg</option>
|
||||
<option value="mg">mg</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="prc-dose-range" class="prc-field__hint"></div>
|
||||
</div>
|
||||
|
||||
<!-- Frequency (informational) -->
|
||||
<div id="prc-frequency-row" class="prc-field prc-field--hidden">
|
||||
<label class="prc-field__label">
|
||||
<?php esc_html_e( 'Typical Frequency', 'peptide-reconstitution-calculator' ); ?>
|
||||
</label>
|
||||
<span id="prc-frequency-value" class="prc-field__static"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results panel -->
|
||||
<div id="prc-results" class="prc-results prc-results--hidden">
|
||||
<h4 class="prc-results__title">
|
||||
<?php esc_html_e( 'Results', 'peptide-reconstitution-calculator' ); ?>
|
||||
</h4>
|
||||
<div class="prc-results__grid">
|
||||
<div class="prc-result-card">
|
||||
<div class="prc-result-card__value" id="prc-result-concentration">—</div>
|
||||
<div class="prc-result-card__label"><?php esc_html_e( 'Concentration (mg/mL)', 'peptide-reconstitution-calculator' ); ?></div>
|
||||
</div>
|
||||
<div class="prc-result-card">
|
||||
<div class="prc-result-card__value" id="prc-result-mcg-per-unit">—</div>
|
||||
<div class="prc-result-card__label"><?php esc_html_e( 'mcg per Syringe Unit', 'peptide-reconstitution-calculator' ); ?></div>
|
||||
</div>
|
||||
<div class="prc-result-card prc-result-card--primary">
|
||||
<div class="prc-result-card__value" id="prc-result-syringe-units">—</div>
|
||||
<div class="prc-result-card__label"><?php esc_html_e( 'Syringe Units to Inject', 'peptide-reconstitution-calculator' ); ?></div>
|
||||
</div>
|
||||
<div class="prc-result-card">
|
||||
<div class="prc-result-card__value" id="prc-result-injection-ml">—</div>
|
||||
<div class="prc-result-card__label"><?php esc_html_e( 'Injection Volume (mL)', 'peptide-reconstitution-calculator' ); ?></div>
|
||||
</div>
|
||||
<div class="prc-result-card">
|
||||
<div class="prc-result-card__value" id="prc-result-doses">—</div>
|
||||
<div class="prc-result-card__label"><?php esc_html_e( 'Doses Per Vial', 'peptide-reconstitution-calculator' ); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Disclaimer -->
|
||||
<div class="prc-disclaimer" id="prc-disclaimer"></div>
|
||||
|
||||
<noscript>
|
||||
<p class="prc-noscript">
|
||||
<?php esc_html_e( 'This calculator requires JavaScript to function.', 'peptide-reconstitution-calculator' ); ?>
|
||||
</p>
|
||||
</noscript>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user