HEX
Server: Apache/2.4.68 (Debian)
System: Linux as-cs-widget-demo-us-central1 6.1.0-44-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.164-1 (2026-03-09) x86_64
User: root (0)
PHP: 8.2.32
Disabled: NONE
Upload Files
File: /var/www/kevin-demo/wp-content/plugins/allspice/includes/mealplans-page.php
<?php
// includes/mealplans-page.php
//
// Native meal-plans landing page - the marketing surface for the meal plans a publisher
// builds in Allspice (thefresh20.com/meal-plans/ is the shape being chased). Same
// create/adopt/regenerate lifecycle as the membership page: generated pages are drafts of
// plain editable Gutenberg blocks the publisher owns, with exactly one dynamic piece - the
// existing allspice/mealplan-products grid, which hydrates from the public products API and
// routes every tap into the widget's meal-plan pane.
//
// Detail pages (/meal-plans/classic/-style) are not generated: products change in the
// Allspice dashboard, and auto-managed per-product pages would fight publisher edits. The
// allspice/mealplan-feature block (blocks.php) exists for that instead - the publisher makes
// a page at whatever slug they like and drops the block in, configured with a product id.

if (!defined('ABSPATH')) exit;

const ALLSPICE_MEALPLANS_PAGE_OPTION = 'allspice_mealplans_page_id';
const ALLSPICE_CREATE_MEALPLANS_PAGE_ACTION = 'allspice_create_mealplans_page';
const ALLSPICE_REGENERATE_MEALPLANS_PAGE_ACTION = 'allspice_regenerate_mealplans_page';

function allspice_mealplans_page_slug(): string {
    $slug = sanitize_title((string)apply_filters('allspice_mealplans_page_slug', 'meal-plans'));
    return $slug !== '' ? $slug : 'meal-plans';
}

function allspice_mealplans_page_title(): string {
    $title = trim((string)apply_filters('allspice_mealplans_page_title', 'Meal Plans'));
    return $title !== '' ? $title : 'Meal Plans';
}

/* ---------------------------------------------------------------------- page content */

/*
 * The generated page: core blocks the publisher edits like any page content, with the
 * products grid as the one dynamic drop-in. Copy is starter copy - every
 * sentence is expected to be rewritten in the site's voice; the structure (hero → grid →
 * how-it-works → testimonial → FAQ → closing CTA) is the part being provided.
 *
 * The closing CTA is a plain core button whose href is the #allspice-mealplans hash - the
 * same deep link the products block and campaign URLs use, handled by the page bundle's
 * hash-action boot. No shortcode, no custom block needed for a CTA.
 */
function allspice_mealplans_page_pattern(): string {
    return <<<HTML
<!-- wp:group {"className":"allspice-mealplans-page","layout":{"type":"constrained"}} -->
<div class="wp-block-group allspice-mealplans-page"><!-- wp:heading {"textAlign":"center","level":1} -->
<h1 class="wp-block-heading has-text-align-center">Meal plans that survive a real week</h1>
<!-- /wp:heading -->

<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center">Every plan comes as complete days of tried-and-true recipes from this site - with the grocery list built for you and step-by-step help while you cook.</p>
<!-- /wp:paragraph -->

<!-- wp:allspice/mealplan-products {"title":"Choose your plan","subtitle":"Tap a plan to preview the full week."} /-->

<!-- wp:heading {"textAlign":"center","level":2} -->
<h2 class="wp-block-heading has-text-align-center">What comes with every plan</h2>
<!-- /wp:heading -->

<!-- wp:columns -->
<div class="wp-block-columns"><!-- wp:column -->
<div class="wp-block-column"><!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">A full week, planned</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Complete days of breakfasts, lunches and dinners - not a pile of links. Open any day to see exactly what's cooking.</p>
<!-- /wp:paragraph --></div>
<!-- /wp:column -->

<!-- wp:column -->
<div class="wp-block-column"><!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">Groceries build themselves</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>One tap turns any plan - or any single day - into an organized grocery list, combined and sorted by aisle.</p>
<!-- /wp:paragraph --></div>
<!-- /wp:column -->

<!-- wp:column -->
<div class="wp-block-column"><!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">Help while you cook</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Every recipe opens with guided cooking and an assistant that answers substitutions, scaling and "can I make this ahead?" on the spot.</p>
<!-- /wp:paragraph --></div>
<!-- /wp:column --></div>
<!-- /wp:columns -->

<!-- wp:heading {"textAlign":"center","level":2} -->
<h2 class="wp-block-heading has-text-align-center">What cooks are saying</h2>
<!-- /wp:heading -->

<!-- wp:columns {"className":"allspice-mp-testimonials"} -->
<div class="wp-block-columns allspice-mp-testimonials"><!-- wp:column {"className":"allspice-mp-testimonial"} -->
<div class="wp-block-column allspice-mp-testimonial"><!-- wp:paragraph {"className":"allspice-mp-testimonial__stars"} -->
<p class="allspice-mp-testimonial__stars" aria-hidden="true">★★★★★</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph {"className":"allspice-mp-testimonial__quote"} -->
<p class="allspice-mp-testimonial__quote">"[Replace this placeholder with a real reader's words - what did these plans change about their week?]"</p>
<!-- /wp:paragraph -->

<!-- wp:separator {"className":"allspice-mp-testimonial__rule"} -->
<hr class="wp-block-separator has-alpha-channel-opacity allspice-mp-testimonial__rule"/>
<!-- /wp:separator -->

<!-- wp:paragraph {"className":"allspice-mp-testimonial__name"} -->
<p class="allspice-mp-testimonial__name"><strong>[Reader name]</strong></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph {"className":"allspice-mp-testimonial__role"} -->
<p class="allspice-mp-testimonial__role">[e.g. Cooking these plans since 2023]</p>
<!-- /wp:paragraph --></div>
<!-- /wp:column -->

<!-- wp:column {"className":"allspice-mp-testimonial"} -->
<div class="wp-block-column allspice-mp-testimonial"><!-- wp:paragraph {"className":"allspice-mp-testimonial__stars"} -->
<p class="allspice-mp-testimonial__stars" aria-hidden="true">★★★★★</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph {"className":"allspice-mp-testimonial__quote"} -->
<p class="allspice-mp-testimonial__quote">"[A second reader's words - the grocery list, the time saved, the picky eater won over.]"</p>
<!-- /wp:paragraph -->

<!-- wp:separator {"className":"allspice-mp-testimonial__rule"} -->
<hr class="wp-block-separator has-alpha-channel-opacity allspice-mp-testimonial__rule"/>
<!-- /wp:separator -->

<!-- wp:paragraph {"className":"allspice-mp-testimonial__name"} -->
<p class="allspice-mp-testimonial__name"><strong>[Reader name]</strong></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph {"className":"allspice-mp-testimonial__role"} -->
<p class="allspice-mp-testimonial__role">[e.g. Feeding a family of five]</p>
<!-- /wp:paragraph --></div>
<!-- /wp:column -->

<!-- wp:column {"className":"allspice-mp-testimonial"} -->
<div class="wp-block-column allspice-mp-testimonial"><!-- wp:paragraph {"className":"allspice-mp-testimonial__stars"} -->
<p class="allspice-mp-testimonial__stars" aria-hidden="true">★★★★★</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph {"className":"allspice-mp-testimonial__quote"} -->
<p class="allspice-mp-testimonial__quote">"[A third reader's words - keep it short and specific; one true sentence beats three vague ones.]"</p>
<!-- /wp:paragraph -->

<!-- wp:separator {"className":"allspice-mp-testimonial__rule"} -->
<hr class="wp-block-separator has-alpha-channel-opacity allspice-mp-testimonial__rule"/>
<!-- /wp:separator -->

<!-- wp:paragraph {"className":"allspice-mp-testimonial__name"} -->
<p class="allspice-mp-testimonial__name"><strong>[Reader name]</strong></p>
<!-- /wp:paragraph -->

<!-- wp:paragraph {"className":"allspice-mp-testimonial__role"} -->
<p class="allspice-mp-testimonial__role">[e.g. Longtime reader]</p>
<!-- /wp:paragraph --></div>
<!-- /wp:column --></div>
<!-- /wp:columns -->

<!-- wp:heading {"textAlign":"center","level":2} -->
<h2 class="wp-block-heading has-text-align-center">Frequently Asked Questions</h2>
<!-- /wp:heading -->

<!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">How do I use a meal plan?</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Tap any plan above to preview the full week. When you're ready, add it to your planner - you can swap individual meals, change serving sizes, and generate the grocery list from there.</p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">Do I need an account?</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>A free account keeps your plans, grocery lists and saved recipes together on every device. You can browse the plans without one.</p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">Can I change a plan after adding it?</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Yes - every plan becomes fully yours once added. Swap meals in or out, move days around, and the grocery list keeps up.</p>
<!-- /wp:paragraph -->

<!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"}} -->
<div class="wp-block-buttons"><!-- wp:button -->
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="#allspice-mealplans">Browse the meal plans</a></div>
<!-- /wp:button --></div>
<!-- /wp:buttons --></div>
<!-- /wp:group -->
HTML;
}

/* Container + testimonial-card styling for the generated page, riding the always-loaded gate
   stylesheet handle exactly as the membership page does. The pattern must not depend on the
   theme supplying a constrained layout: classic/full-width page canvases (FitCook) rendered
   every section 0px from the viewport edge because nothing anywhere set a max-width. Scoped
   under the wrapper class and kept minimal, so theme/publisher styling still wins the rest. */
add_action('wp_enqueue_scripts', 'allspice_mealplans_page_styles', 22);
function allspice_mealplans_page_styles(): void {
    wp_add_inline_style('allspice-gate',
        '.allspice-mealplans-page{max-width:1120px;margin:0 auto;padding:8px 20px 28px;box-sizing:border-box}'
        . '.allspice-mealplans-page h1.wp-block-heading{margin:10px 0 8px}'
        . '.allspice-mealplans-page h2.wp-block-heading{margin:40px 0 16px}'
        /* Testimonials: card grid (stars, quote, rule pinned to the bottom, name, tenure) so
           the three names align even when quotes run different lengths. */
        . '.allspice-mp-testimonials{gap:20px}'
        . '.allspice-mp-testimonials .allspice-mp-testimonial{background:#f7f2f4;border-radius:16px;padding:26px 24px;display:flex;flex-direction:column}'
        . '.allspice-mp-testimonial .allspice-mp-testimonial__stars{color:#7c3a5c;font-size:15px;letter-spacing:3px;margin:0 0 12px}'
        . '.allspice-mp-testimonial .allspice-mp-testimonial__quote{margin:0 0 18px;line-height:1.55}'
        . '.allspice-mp-testimonial .allspice-mp-testimonial__rule{margin:auto 0 14px;width:100%;max-width:none;border:0;border-top:1px solid rgba(0,0,0,.12);opacity:1}'
        . '.allspice-mp-testimonial .allspice-mp-testimonial__name{margin:0}'
        . '.allspice-mp-testimonial .allspice-mp-testimonial__role{margin:2px 0 0;font-size:.9em;opacity:.65}'
    );
}

function allspice_mealplans_page_default_content(): string {
    if (function_exists('register_block_type')) {
        return allspice_mealplans_page_pattern();
    }
    /* A WP too old for blocks still gets a working page: the hash deep link alone. */
    return '<p><a href="#allspice-mealplans">Browse the meal plans</a></p>';
}

/* ------------------------------------------------------------------------ admin creation */

/* The saved meal-plans page when it is still a valid, non-trashed page - else null. */
function allspice_mealplans_page_current() {
    $saved = (int)get_option(ALLSPICE_MEALPLANS_PAGE_OPTION, 0);
    if ($saved <= 0) return null;
    $post = get_post($saved);
    if (!$post || $post->post_type !== 'page' || $post->post_status === 'trash') return null;
    return $post;
}

function allspice_mealplans_page_editor_url(int $page_id): string {
    return admin_url('post.php?post=' . $page_id . '&action=edit');
}

/*
 * Create Meal Plans Page - same contract as the membership page: idempotent, never
 * publishes, never overwrites publisher edits.
 *   1. valid saved page id  -> reuse
 *   2. page at the slug     -> adopt (store its id)
 *   3. otherwise            -> create a DRAFT page from the pattern
 */
function allspice_create_mealplans_page(bool $force_create = false): array {
    if (!$force_create) {
        $current = allspice_mealplans_page_current();
        if ($current !== null) {
            return ['ok' => true, 'page_id' => (int)$current->ID, 'created' => false, 'reason' => 'reused_saved_id'];
        }
        $slug = allspice_mealplans_page_slug();
        $by_slug = function_exists('get_page_by_path') ? get_page_by_path($slug, OBJECT, 'page') : null;
        if ($by_slug && $by_slug->post_status !== 'trash') {
            update_option(ALLSPICE_MEALPLANS_PAGE_OPTION, (int)$by_slug->ID, false);
            return ['ok' => true, 'page_id' => (int)$by_slug->ID, 'created' => false, 'reason' => 'adopted_slug_page'];
        }
    }
    $page_id = wp_insert_post([
        'post_type' => 'page',
        'post_status' => 'draft',           // NEVER published automatically
        'post_title' => allspice_mealplans_page_title(),
        'post_name' => allspice_mealplans_page_slug(),
        'post_content' => allspice_mealplans_page_default_content(),
    ], true);
    if (is_wp_error($page_id) || (int)$page_id <= 0) {
        return ['ok' => false, 'page_id' => 0, 'created' => false, 'reason' => 'insert_failed'];
    }
    update_option(ALLSPICE_MEALPLANS_PAGE_OPTION, (int)$page_id, false);
    return ['ok' => true, 'page_id' => (int)$page_id, 'created' => true, 'reason' => 'created_draft'];
}

/*
 * Regenerate: current generated page goes to Trash (recoverable - publisher edits are never
 * destroyed), then a fresh draft is created from the latest pattern.
 */
function allspice_regenerate_mealplans_page(): array {
    $current = allspice_mealplans_page_current();
    if ($current !== null) {
        wp_delete_post((int)$current->ID, false); /* false = Trash, NEVER permanent delete */
    }
    delete_option(ALLSPICE_MEALPLANS_PAGE_OPTION);
    $r = allspice_create_mealplans_page(true);
    $r['trashed_page_id'] = $current !== null ? (int)$current->ID : 0;
    return $r;
}

add_action('admin_post_' . ALLSPICE_CREATE_MEALPLANS_PAGE_ACTION, 'allspice_admin_post_create_mealplans_page');
function allspice_admin_post_create_mealplans_page(): void {
    if (!current_user_can('manage_options')) wp_die('Forbidden', 403);
    check_admin_referer(ALLSPICE_CREATE_MEALPLANS_PAGE_ACTION);
    $r = allspice_create_mealplans_page();
    if (!empty($r['ok']) && (int)$r['page_id'] > 0) {
        wp_safe_redirect(allspice_mealplans_page_editor_url((int)$r['page_id']));
        exit;
    }
    set_transient('allspice_admin_notice', ['msg' => 'Could not create the meal plans page.', 'type' => 'error'], 30);
    wp_safe_redirect(admin_url('options-general.php?page=' . ALLSPICE_SETTINGS_PAGE));
    exit;
}

add_action('admin_post_' . ALLSPICE_REGENERATE_MEALPLANS_PAGE_ACTION, 'allspice_admin_post_regenerate_mealplans_page');
function allspice_admin_post_regenerate_mealplans_page(): void {
    if (!current_user_can('manage_options')) wp_die('Forbidden', 403);
    check_admin_referer(ALLSPICE_REGENERATE_MEALPLANS_PAGE_ACTION);
    $r = allspice_regenerate_mealplans_page();
    if (!empty($r['ok']) && (int)$r['page_id'] > 0) {
        wp_safe_redirect(allspice_mealplans_page_editor_url((int)$r['page_id']));
        exit;
    }
    set_transient('allspice_admin_notice', ['msg' => 'Could not regenerate the meal plans page.', 'type' => 'error'], 30);
    wp_safe_redirect(admin_url('options-general.php?page=' . ALLSPICE_SETTINGS_PAGE));
    exit;
}