From 9be80400e40ace0800e32c2e2c4f9406577038fd Mon Sep 17 00:00:00 2001 From: Muryam <6983531+muryamsultana@users.noreply.github.com> Date: Sun, 12 Jul 2026 14:31:03 +0500 Subject: [PATCH 1/3] Add PlanForge - editorial calendar app Single-file GPLv2 plugin rendering a standalone editorial calendar at /calendar/: month grid of real posts color-coded by status, hover quick-add for draft ideas, and drag-and-drop rescheduling (published posts moved ahead correctly become scheduled). Fully local, no external requests. Tested via @wp-playground/cli (PHP 8.3, latest WP). Co-Authored-By: Claude Fable 5 --- blueprints/planforge/blueprint.json | 43 ++++ blueprints/planforge/planforge.php | 370 ++++++++++++++++++++++++++++ 2 files changed, 413 insertions(+) create mode 100644 blueprints/planforge/blueprint.json create mode 100644 blueprints/planforge/planforge.php diff --git a/blueprints/planforge/blueprint.json b/blueprints/planforge/blueprint.json new file mode 100644 index 00000000..b9dedfb8 --- /dev/null +++ b/blueprints/planforge/blueprint.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://playground.wordpress.net/blueprint-schema.json", + "meta": { + "title": "PlanForge", + "description": "An editorial calendar for your posts. See your publishing month at a glance, quick-add draft ideas on any day, and drag posts between days to reschedule.", + "author": "muryamsultana", + "categories": [ + "Apps", + "Productivity" + ] + }, + "landingPage": "/calendar/", + "preferredVersions": { + "php": "8.2", + "wp": "latest" + }, + "login": true, + "steps": [ + { + "step": "mkdir", + "path": "/wordpress/wp-content/plugins/planforge" + }, + { + "step": "writeFile", + "path": "/wordpress/wp-content/plugins/planforge/planforge.php", + "data": { + "resource": "url", + "url": "https://raw.githubusercontent.com/wordpress/blueprints/PlanForge/blueprints/planforge/planforge.php" + } + }, + { + "step": "activatePlugin", + "pluginPath": "planforge/planforge.php" + }, + { + "step": "setSiteOptions", + "options": { + "blogname": "PlanForge", + "blogdescription": "Plan your publishing month" + } + } + ] +} diff --git a/blueprints/planforge/planforge.php b/blueprints/planforge/planforge.php new file mode 100644 index 00000000..2526f995 --- /dev/null +++ b/blueprints/planforge/planforge.php @@ -0,0 +1,370 @@ + 'page', + 'post_status' => 'publish', + 'post_title' => __( 'Calendar', 'planforge' ), + 'post_name' => self::PAGE_SLUG, + 'post_content' => '', + ) + ); + } + flush_rewrite_rules(); + } + + public static function admin_menu() { + add_menu_page( + __( 'Calendar', 'planforge' ), + __( 'Calendar', 'planforge' ), + 'edit_posts', + 'planforge', + function () { + wp_safe_redirect( home_url( '/' . self::PAGE_SLUG . '/' ) ); + exit; + }, + 'dashicons-calendar-alt', + 32 + ); + } + + // ---------------------------------------------------------------- ajax -- + + private static function check_request() { + if ( ! current_user_can( 'edit_posts' ) || ! check_ajax_referer( self::NONCE, 'nonce', false ) ) { + wp_send_json_error( array( 'message' => 'Not allowed' ), 403 ); + } + } + + private static function valid_date( $date ) { + return (bool) preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ); + } + + public static function ajax_add() { + self::check_request(); + $title = sanitize_text_field( wp_unslash( $_POST['title'] ?? '' ) ); + $date = sanitize_text_field( wp_unslash( $_POST['date'] ?? '' ) ); + if ( '' === $title || ! self::valid_date( $date ) ) { + wp_send_json_error( array( 'message' => 'Title and date required' ), 400 ); + } + $id = wp_insert_post( + array( + 'post_type' => 'post', + 'post_status' => 'draft', + 'post_title' => $title, + 'post_date' => $date . ' 09:00:00', + ), + true + ); + if ( is_wp_error( $id ) ) { + wp_send_json_error( array( 'message' => $id->get_error_message() ), 500 ); + } + wp_send_json_success( array( 'id' => $id ) ); + } + + public static function ajax_move() { + self::check_request(); + $post_id = (int) ( $_POST['post'] ?? 0 ); + $date = sanitize_text_field( wp_unslash( $_POST['date'] ?? '' ) ); + $post = get_post( $post_id ); + + if ( ! $post || 'post' !== $post->post_type || ! self::valid_date( $date ) || ! current_user_can( 'edit_post', $post_id ) ) { + wp_send_json_error( array( 'message' => 'Bad request' ), 400 ); + } + + $time = gmdate( 'H:i:s', strtotime( $post->post_date ) ); + $result = wp_update_post( + array( + 'ID' => $post_id, + 'post_date' => $date . ' ' . $time, + 'post_date_gmt' => get_gmt_from_date( $date . ' ' . $time ), + 'edit_date' => true, + ), + true + ); + if ( is_wp_error( $result ) ) { + wp_send_json_error( array( 'message' => $result->get_error_message() ), 500 ); + } + wp_send_json_success( array( 'status' => get_post_status( $post_id ) ) ); + } + + // -------------------------------------------------------------- render -- + + public static function maybe_render_app() { + if ( ! is_page( self::PAGE_SLUG ) ) { + return; + } + if ( ! is_user_logged_in() ) { + auth_redirect(); + } + self::render_app(); + exit; + } + + private static function month_posts( $year, $month ) { + $posts = get_posts( + array( + 'post_type' => 'post', + 'post_status' => self::$statuses, + 'posts_per_page' => -1, + 'orderby' => 'date', + 'order' => 'ASC', + 'date_query' => array( + array( + 'year' => $year, + 'month' => $month, + ), + ), + ) + ); + $by_day = array(); + foreach ( $posts as $post ) { + $day = gmdate( 'Y-m-d', strtotime( $post->post_date ) ); + $by_day[ $day ][] = $post; + } + return $by_day; + } + + private static function render_app() { + $today = current_time( 'Y-m-d' ); + // Note: WP reserves the `m` query var for date archives, so use `pf_month`. + $m = isset( $_GET['pf_month'] ) && preg_match( '/^\d{4}-(0[1-9]|1[0-2])$/', $_GET['pf_month'] ) ? sanitize_text_field( wp_unslash( $_GET['pf_month'] ) ) : substr( $today, 0, 7 ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + list( $year, $month ) = array_map( 'intval', explode( '-', $m ) ); + + $first_ts = mktime( 0, 0, 0, $month, 1, $year ); + $days_total = (int) gmdate( 't', $first_ts ); + $start_dow = (int) gmdate( 'w', $first_ts ); // 0 = Sunday + $prev = gmdate( 'Y-m', mktime( 0, 0, 0, $month - 1, 1, $year ) ); + $next = gmdate( 'Y-m', mktime( 0, 0, 0, $month + 1, 1, $year ) ); + $by_day = self::month_posts( $year, $month ); + $nonce = wp_create_nonce( self::NONCE ); + $app_url = home_url( '/' . self::PAGE_SLUG . '/' ); + + $counts = array( 'publish' => 0, 'future' => 0, 'draft' => 0, 'pending' => 0 ); + foreach ( $by_day as $day_posts ) { + foreach ( $day_posts as $p ) { + if ( isset( $counts[ $p->post_status ] ) ) { + $counts[ $p->post_status ]++; + } + } + } + + $labels = array( + 'publish' => __( 'Published', 'planforge' ), + 'future' => __( 'Scheduled', 'planforge' ), + 'draft' => __( 'Draft', 'planforge' ), + 'pending' => __( 'Pending', 'planforge' ), + ); + + header( 'Content-Type: text/html; charset=utf-8' ); + ?> + +> + + + +<?php esc_html_e( 'PlanForge', 'planforge' ); ?> + + + +
+
+

🗓️ PlanForge

+
+ + + +
+
+ +
+ $label ) : ?> + : + +
+ +
+ ' . esc_html( $dow ) . '
'; + } + $cells = (int) ceil( ( $start_dow + $days_total ) / 7 ) * 7; + for ( $i = 0; $i < $cells; $i++ ) { + $day_num = $i - $start_dow + 1; + if ( $day_num < 1 || $day_num > $days_total ) { + echo '
'; + continue; + } + $date = sprintf( '%04d-%02d-%02d', $year, $month, $day_num ); + printf( + '
%d', + $date === $today ? ' pf-today' : '', + esc_attr( $date ), + (int) $day_num, + esc_attr__( 'Quick-add a draft on this day', 'planforge' ) + ); + foreach ( $by_day[ $date ] ?? array() as $p ) { + printf( + '
%s
', + esc_attr( $p->post_status ), + (int) $p->ID, + esc_attr( $labels[ $p->post_status ] ?? $p->post_status ), + esc_attr( gmdate( 'H:i', strtotime( $p->post_date ) ) ), + esc_html( $p->post_title ? $p->post_title : __( '(no title)', 'planforge' ) ), + esc_url( admin_url( 'post.php?post=' . $p->ID . '&action=edit' ) ), + esc_attr__( 'Edit post', 'planforge' ) + ); + } + echo '
'; + } + ?> +
+ +

+ + + + + Date: Sun, 12 Jul 2026 14:41:31 +0500 Subject: [PATCH 2/3] Use bundled resource for the PlanForge plugin file Match the reviewed HabitForge layout: plugin lives in plugin/ and the blueprint references it with a bundled resource instead of a branch-specific raw.githubusercontent.com URL. Co-Authored-By: Claude Fable 5 --- blueprints/planforge/blueprint.json | 4 ++-- blueprints/planforge/{ => plugin}/planforge.php | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename blueprints/planforge/{ => plugin}/planforge.php (100%) diff --git a/blueprints/planforge/blueprint.json b/blueprints/planforge/blueprint.json index b9dedfb8..1cfa0afd 100644 --- a/blueprints/planforge/blueprint.json +++ b/blueprints/planforge/blueprint.json @@ -24,8 +24,8 @@ "step": "writeFile", "path": "/wordpress/wp-content/plugins/planforge/planforge.php", "data": { - "resource": "url", - "url": "https://raw.githubusercontent.com/wordpress/blueprints/PlanForge/blueprints/planforge/planforge.php" + "resource": "bundled", + "path": "/plugin/planforge.php" } }, { diff --git a/blueprints/planforge/planforge.php b/blueprints/planforge/plugin/planforge.php similarity index 100% rename from blueprints/planforge/planforge.php rename to blueprints/planforge/plugin/planforge.php From 4d02d638596c3869e4a79cf266bdf9e4e325c1c3 Mon Sep 17 00:00:00 2001 From: Muryam <6983531+muryamsultana@users.noreply.github.com> Date: Sun, 12 Jul 2026 14:51:56 +0500 Subject: [PATCH 3/3] Rename PlanForge to PostsForge Co-Authored-By: Claude Fable 5 --- .../{planforge => postsforge}/blueprint.json | 12 ++--- .../plugin/postsforge.php} | 46 +++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) rename blueprints/{planforge => postsforge}/blueprint.json (73%) rename blueprints/{planforge/plugin/planforge.php => postsforge/plugin/postsforge.php} (92%) diff --git a/blueprints/planforge/blueprint.json b/blueprints/postsforge/blueprint.json similarity index 73% rename from blueprints/planforge/blueprint.json rename to blueprints/postsforge/blueprint.json index 1cfa0afd..418c87f4 100644 --- a/blueprints/planforge/blueprint.json +++ b/blueprints/postsforge/blueprint.json @@ -1,7 +1,7 @@ { "$schema": "https://playground.wordpress.net/blueprint-schema.json", "meta": { - "title": "PlanForge", + "title": "PostsForge", "description": "An editorial calendar for your posts. See your publishing month at a glance, quick-add draft ideas on any day, and drag posts between days to reschedule.", "author": "muryamsultana", "categories": [ @@ -18,24 +18,24 @@ "steps": [ { "step": "mkdir", - "path": "/wordpress/wp-content/plugins/planforge" + "path": "/wordpress/wp-content/plugins/postsforge" }, { "step": "writeFile", - "path": "/wordpress/wp-content/plugins/planforge/planforge.php", + "path": "/wordpress/wp-content/plugins/postsforge/postsforge.php", "data": { "resource": "bundled", - "path": "/plugin/planforge.php" + "path": "/plugin/postsforge.php" } }, { "step": "activatePlugin", - "pluginPath": "planforge/planforge.php" + "pluginPath": "postsforge/postsforge.php" }, { "step": "setSiteOptions", "options": { - "blogname": "PlanForge", + "blogname": "PostsForge", "blogdescription": "Plan your publishing month" } } diff --git a/blueprints/planforge/plugin/planforge.php b/blueprints/postsforge/plugin/postsforge.php similarity index 92% rename from blueprints/planforge/plugin/planforge.php rename to blueprints/postsforge/plugin/postsforge.php index 2526f995..0c5ddc80 100644 --- a/blueprints/planforge/plugin/planforge.php +++ b/blueprints/postsforge/plugin/postsforge.php @@ -1,23 +1,23 @@ 'page', 'post_status' => 'publish', - 'post_title' => __( 'Calendar', 'planforge' ), + 'post_title' => __( 'Calendar', 'postsforge' ), 'post_name' => self::PAGE_SLUG, 'post_content' => '', ) @@ -46,10 +46,10 @@ public static function activate() { public static function admin_menu() { add_menu_page( - __( 'Calendar', 'planforge' ), - __( 'Calendar', 'planforge' ), + __( 'Calendar', 'postsforge' ), + __( 'Calendar', 'postsforge' ), 'edit_posts', - 'planforge', + 'postsforge', function () { wp_safe_redirect( home_url( '/' . self::PAGE_SLUG . '/' ) ); exit; @@ -181,10 +181,10 @@ private static function render_app() { } $labels = array( - 'publish' => __( 'Published', 'planforge' ), - 'future' => __( 'Scheduled', 'planforge' ), - 'draft' => __( 'Draft', 'planforge' ), - 'pending' => __( 'Pending', 'planforge' ), + 'publish' => __( 'Published', 'postsforge' ), + 'future' => __( 'Scheduled', 'postsforge' ), + 'draft' => __( 'Draft', 'postsforge' ), + 'pending' => __( 'Pending', 'postsforge' ), ); header( 'Content-Type: text/html; charset=utf-8' ); @@ -194,7 +194,7 @@ private static function render_app() { -<?php esc_html_e( 'PlanForge', 'planforge' ); ?> +<?php esc_html_e( 'PostsForge', 'postsforge' ); ?>