diff --git a/blueprints/postsforge/blueprint.json b/blueprints/postsforge/blueprint.json new file mode 100644 index 00000000..418c87f4 --- /dev/null +++ b/blueprints/postsforge/blueprint.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://playground.wordpress.net/blueprint-schema.json", + "meta": { + "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": [ + "Apps", + "Productivity" + ] + }, + "landingPage": "/calendar/", + "preferredVersions": { + "php": "8.2", + "wp": "latest" + }, + "login": true, + "steps": [ + { + "step": "mkdir", + "path": "/wordpress/wp-content/plugins/postsforge" + }, + { + "step": "writeFile", + "path": "/wordpress/wp-content/plugins/postsforge/postsforge.php", + "data": { + "resource": "bundled", + "path": "/plugin/postsforge.php" + } + }, + { + "step": "activatePlugin", + "pluginPath": "postsforge/postsforge.php" + }, + { + "step": "setSiteOptions", + "options": { + "blogname": "PostsForge", + "blogdescription": "Plan your publishing month" + } + } + ] +} diff --git a/blueprints/postsforge/plugin/postsforge.php b/blueprints/postsforge/plugin/postsforge.php new file mode 100644 index 00000000..0c5ddc80 --- /dev/null +++ b/blueprints/postsforge/plugin/postsforge.php @@ -0,0 +1,370 @@ + 'page', + 'post_status' => 'publish', + 'post_title' => __( 'Calendar', 'postsforge' ), + 'post_name' => self::PAGE_SLUG, + 'post_content' => '', + ) + ); + } + flush_rewrite_rules(); + } + + public static function admin_menu() { + add_menu_page( + __( 'Calendar', 'postsforge' ), + __( 'Calendar', 'postsforge' ), + 'edit_posts', + 'postsforge', + 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', 'postsforge' ), + 'future' => __( 'Scheduled', 'postsforge' ), + 'draft' => __( 'Draft', 'postsforge' ), + 'pending' => __( 'Pending', 'postsforge' ), + ); + + header( 'Content-Type: text/html; charset=utf-8' ); + ?> + +> + + + +<?php esc_html_e( 'PostsForge', 'postsforge' ); ?> + + + +
+
+

🗓️ PostsForge

+
+ + + +
+
+ +
+ $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', 'postsforge' ) + ); + 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)', 'postsforge' ) ), + esc_url( admin_url( 'post.php?post=' . $p->ID . '&action=edit' ) ), + esc_attr__( 'Edit post', 'postsforge' ) + ); + } + echo '
'; + } + ?> +
+ +

+ + + + +