From c5067f3e2ec518967a916eccca05959ded1b37eb Mon Sep 17 00:00:00 2001 From: Omar Alshaker Date: Wed, 22 Apr 2026 22:28:10 +0200 Subject: [PATCH 01/18] Add On This Day Widget --- src/wp-admin/css/on-this-day.css | 345 ++++++++++++++++++ .../includes/class-wp-on-this-day.php | 283 ++++++++++++++ src/wp-admin/includes/dashboard.php | 17 + src/wp-includes/script-loader.php | 2 + tools/seed-otd-posts.php | 114 ++++++ 5 files changed, 761 insertions(+) create mode 100644 src/wp-admin/css/on-this-day.css create mode 100644 src/wp-admin/includes/class-wp-on-this-day.php create mode 100644 tools/seed-otd-posts.php diff --git a/src/wp-admin/css/on-this-day.css b/src/wp-admin/css/on-this-day.css new file mode 100644 index 0000000000000..51a72dda3fea4 --- /dev/null +++ b/src/wp-admin/css/on-this-day.css @@ -0,0 +1,345 @@ +/* On This Day dashboard widget + ========================================================================== */ + +#dashboard_on_this_day .inside { + margin: 0; + padding: 0; + max-height: 560px; + overflow: auto; +} + +#dashboard_on_this_day .hndle { + gap: 0; +} + +.on-this-day-title-main { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.on-this-day-title-main::before { + content: ""; + display: inline-block; + width: 16px; + height: 16px; + background-color: currentColor; + opacity: 0.85; + -webkit-mask: url("data:image/svg+xml;utf8,") center/contain no-repeat; + mask: url("data:image/svg+xml;utf8,") center/contain no-repeat; +} + +.on-this-day-title-date { + display: inline-block; + margin-left: 10px; + padding: 2px 9px; + font-weight: 600; + font-size: 11px; + letter-spacing: 0.3px; + text-transform: uppercase; + color: #135e96; + background: #e7f1fb; + border-radius: 999px; + vertical-align: 1px; +} + +.on-this-day-widget { + font-size: 13px; + color: #1d2327; +} + +.on-this-day-intro { + margin: 0; + padding: 14px 16px 12px; + color: #50575e; + background: linear-gradient(180deg, #f6f7ff 0%, #ffffff 100%); + border-bottom: 1px solid #f0f0f1; +} + +.on-this-day-intro strong { + color: #1d2327; +} + +.on-this-day-timeline { + margin: 0; + padding: 6px 16px 16px; + position: relative; + list-style: none; +} + +.on-this-day-timeline::before { + content: ""; + position: absolute; + left: 38px; + top: 16px; + bottom: 8px; + width: 2px; + background: linear-gradient(180deg, #c3c4c7 0%, #dcdcde 60%, transparent 100%); + border-radius: 2px; +} + +.on-this-day-year-group { + position: relative; + margin: 14px 0 0; + padding-left: 64px; + list-style: none; +} + +.on-this-day-year-group:first-child { + margin-top: 8px; +} + +.on-this-day-year-badge { + position: absolute; + left: 0; + top: 0; + width: 54px; + height: 44px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 0; + background: #fff; + border: 1px solid #dcdcde; + border-radius: 10px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); + z-index: 1; + box-sizing: border-box; +} + +.on-this-day-year-group:first-child .on-this-day-year-badge { + background: linear-gradient(135deg, #2271b1 0%, #135e96 100%); + border-color: transparent; + color: #fff; + box-shadow: 0 2px 6px rgba(34, 113, 177, 0.35); +} + +.on-this-day-year-group:first-child .on-this-day-year-ago { + color: rgba(255, 255, 255, 0.85); +} + +.on-this-day-year-number { + font-weight: 700; + font-size: 14px; + letter-spacing: 0.2px; + line-height: 1.15; +} + +.on-this-day-year-ago { + margin-top: 1px; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: #646970; + font-weight: 500; + white-space: nowrap; +} + +.on-this-day-post-list { + margin: 0; + padding: 0; + list-style: none; + display: flex; + flex-direction: column; + gap: 8px; +} + +.on-this-day-post { + display: flex; + gap: 12px; + padding: 10px 12px; + background: #fff; + border: 1px solid #f0f0f1; + border-radius: 8px; + transition: border-color .15s ease, box-shadow .15s ease; +} + +.on-this-day-post:hover { + border-color: #c3c4c7; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.on-this-day-post-thumbnail { + flex: 0 0 52px; + width: 52px; + height: 52px; + border-radius: 6px; + overflow: hidden; + background: #f0f0f1; + display: block; +} + +.on-this-day-post-thumbnail img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.on-this-day-post-body { + flex: 1 1 auto; + min-width: 0; +} + +.on-this-day-post-title { + margin: 0 0 3px; + font-size: 13px; + font-weight: 600; + line-height: 1.35; +} + +.on-this-day-post-title a { + color: #1d2327; + text-decoration: none; + box-shadow: none; +} + +.on-this-day-post-title a:hover, +.on-this-day-post-title a:focus { + color: #2271b1; +} + +.on-this-day-chip { + display: inline-block; + margin-left: 4px; + padding: 1px 7px; + font-size: 10px; + font-weight: 600; + letter-spacing: 0.3px; + text-transform: uppercase; + border-radius: 999px; + vertical-align: 2px; + white-space: nowrap; +} + +.on-this-day-chip-private { + background: #fcf0f1; + color: #b32d2e; + border: 1px solid #f5c9cc; +} + +.on-this-day-post-excerpt { + margin: 0 0 6px; + color: #50575e; + line-height: 1.5; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.on-this-day-post-meta { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + color: #646970; + font-size: 12px; +} + +.on-this-day-post-time { + display: inline-flex; + align-items: center; + gap: 4px; + color: #646970; +} + +.on-this-day-post-time svg { + vertical-align: middle; + opacity: 0.85; +} + +.on-this-day-post-categories { + padding: 1px 8px; + background: #f6f7f7; + border-radius: 999px; + color: #50575e; + font-size: 11px; + max-width: 200px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.on-this-day-post-actions { + margin-left: auto; + display: inline-flex; + gap: 10px; +} + +.on-this-day-post-action { + color: #2271b1; + text-decoration: none; + font-weight: 500; +} + +.on-this-day-post-action:hover, +.on-this-day-post-action:focus { + color: #135e96; + text-decoration: underline; +} + +/* Empty state */ +.on-this-day-empty { + text-align: center; + padding: 26px 20px 22px; +} + +.on-this-day-empty-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 64px; + height: 64px; + border-radius: 50%; + color: #2271b1; + background: radial-gradient(circle at 30% 30%, #e7f1fb 0%, #f6f7ff 60%, #ffffff 100%); + box-shadow: inset 0 0 0 1px #e0eaf5; + margin-bottom: 10px; +} + +.on-this-day-empty-title { + margin: 6px 0 4px; + font-size: 15px; + font-weight: 600; + color: #1d2327; +} + +.on-this-day-empty-text { + max-width: 340px; + margin: 0 auto 12px; + color: #50575e; + line-height: 1.55; +} + +.on-this-day-empty-cta { + margin: 0; +} + +@media (prefers-reduced-motion: reduce) { + .on-this-day-post { + transition: none; + } +} + +@media screen and (max-width: 600px) { + .on-this-day-timeline { + padding-left: 12px; + padding-right: 12px; + } + + .on-this-day-timeline::before { + left: 34px; + } + + .on-this-day-year-group { + padding-left: 58px; + } + + .on-this-day-post-thumbnail { + flex-basis: 44px; + width: 44px; + height: 44px; + } +} diff --git a/src/wp-admin/includes/class-wp-on-this-day.php b/src/wp-admin/includes/class-wp-on-this-day.php new file mode 100644 index 0000000000000..24f565311dd4f --- /dev/null +++ b/src/wp-admin/includes/class-wp-on-this-day.php @@ -0,0 +1,283 @@ +'; + + if ( empty( $posts ) ) { + self::render_empty_state(); + } else { + self::render_posts( $posts ); + } + + echo ''; + } + + /** + * Retrieves posts by a given author that were published on this + * same month and day in previous years. + * + * @since 7.1.0 + * + * @param int $user_id Author ID to query posts for. + * @return WP_Post[] Array of posts ordered by newest first. + */ + public static function get_posts( $user_id ) { + $args = array( + 'author' => (int) $user_id, + 'post_type' => 'post', + 'post_status' => array( 'publish', 'private' ), + 'posts_per_page' => self::POSTS_PER_PAGE, + 'ignore_sticky_posts' => true, + 'orderby' => 'date', + 'order' => 'DESC', + 'no_found_rows' => true, + ); + + /** + * Filters the arguments used to query posts for the On This Day dashboard widget. + * + * @since 7.1.0 + * + * @param array $args WP_Query arguments. + * @param int $user_id The author ID the query is scoped to. + */ + $args = apply_filters( 'dashboard_on_this_day_query_args', $args, $user_id ); + + add_filter( 'posts_where', array( __CLASS__, 'filter_posts_where' ) ); + $query = new WP_Query( $args ); + remove_filter( 'posts_where', array( __CLASS__, 'filter_posts_where' ) ); + + return $query->posts; + } + + /** + * Restricts the widget's query to the current month and day in prior years. + * + * @since 7.1.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param string $where SQL WHERE clause. + * @return string Filtered WHERE clause. + */ + public static function filter_posts_where( $where ) { + global $wpdb; + + $month = (int) current_time( 'n' ); + $day = (int) current_time( 'j' ); + $year = (int) current_time( 'Y' ); + + $where .= $wpdb->prepare( + " AND MONTH({$wpdb->posts}.post_date) = %d AND DAY({$wpdb->posts}.post_date) = %d AND YEAR({$wpdb->posts}.post_date) < %d", + $month, + $day, + $year + ); + + return $where; + } + + /** + * Renders the empty state shown when no matching posts exist. + * + * @since 7.1.0 + */ + protected static function render_empty_state() { + ?> +
+ +

+

+ ' . esc_html( date_i18n( 'F j' ) ) . '' + ); + ?> +

+

+ + + +

+
+ +

+ ' . esc_html( number_format_i18n( $post_count ) ) . '', + '' . esc_html( number_format_i18n( $year_count ) ) . '' + ); + ?> +

+ + + ID ); + $view_link = get_permalink( $post->ID ); + $title = get_the_title( $post ); + if ( '' === trim( $title ) ) { + $title = __( '(no title)' ); + } + + $excerpt = has_excerpt( $post ) ? $post->post_excerpt : $post->post_content; + $excerpt = wp_strip_all_tags( strip_shortcodes( $excerpt ) ); + $excerpt = preg_replace( '/\s+/', ' ', $excerpt ); + $excerpt = wp_trim_words( trim( $excerpt ), 24, '…' ); + + $time_str = get_the_time( get_option( 'time_format' ), $post ); + $time_iso = get_the_time( 'Y-m-d H:i', $post ); + $status = get_post_status( $post ); + $thumbnail_url = has_post_thumbnail( $post ) ? get_the_post_thumbnail_url( $post, 'thumbnail' ) : ''; + $categories = get_the_category( $post->ID ); + + $classes = array( 'on-this-day-post' ); + if ( $thumbnail_url ) { + $classes[] = 'has-thumbnail'; + } + ?> +
  • + + + +
    +

    + +

    + +

    + + +
    +
  • + %1$s%2$s', + __( 'On This Day' ), + esc_html( date_i18n( 'F j' ) ) + ); + + wp_add_dashboard_widget( 'dashboard_on_this_day', $on_this_day_title, array( 'WP_On_This_Day', 'render_dashboard_widget' ) ); + } + // WordPress Events and News. wp_add_dashboard_widget( 'dashboard_primary', __( 'WordPress Events and News' ), 'wp_dashboard_events_news' ); diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 42d42b3f8781d..74754f413c1ee 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -1634,6 +1634,7 @@ function wp_default_styles( $styles ) { $styles->add( 'l10n', "/wp-admin/css/l10n$suffix.css" ); $styles->add( 'code-editor', "/wp-admin/css/code-editor$suffix.css", array( 'wp-codemirror' ) ); $styles->add( 'site-health', "/wp-admin/css/site-health$suffix.css" ); + $styles->add( 'on-this-day', "/wp-admin/css/on-this-day$suffix.css" ); $styles->add( 'wp-admin', false, array( 'dashicons', 'common', 'forms', 'admin-menu', 'dashboard', 'list-tables', 'edit', 'revisions', 'media', 'themes', 'about', 'nav-menus', 'widgets', 'site-icon', 'l10n', 'wp-base-styles' ) ); @@ -1855,6 +1856,7 @@ function wp_default_styles( $styles ) { 'customize-preview', 'login', 'site-health', + 'on-this-day', 'wp-empty-template-alert', // Includes CSS. 'buttons', diff --git a/tools/seed-otd-posts.php b/tools/seed-otd-posts.php new file mode 100644 index 0000000000000..4b375cfda8bff --- /dev/null +++ b/tools/seed-otd-posts.php @@ -0,0 +1,114 @@ + '2019-04-22 09:12:00', + 'title' => 'Hello, blogosphere', + 'content' => 'First post on a new blog. Today I spun up a tiny WordPress site and wrote my very first words. The plan: show up, write often, and see what happens. Small starts, long timelines.', + 'status' => 'publish', + 'cats' => array( 'personal' ), + ), + array( + 'date' => '2020-04-22 14:02:00', + 'title' => 'Sourdough Diaries, Day 37', + 'content' => 'Another lockdown afternoon, another loaf. The crumb is finally opening up, the crust shatters, and the kitchen smells like a small victory. Recipe adjustments below for anyone still chasing their first ear.', + 'status' => 'publish', + 'cats' => array( 'cooking', 'journal' ), + ), + array( + 'date' => '2021-04-22 18:40:00', + 'title' => 'Notes from my first 10k', + 'content' => 'I did not expect to finish, let alone enjoy it. Somewhere around kilometer seven the legs stopped complaining and the mind went quiet. I am writing this still a little sweaty, grinning.', + 'status' => 'publish', + 'cats' => array( 'running' ), + ), + array( + 'date' => '2022-04-22 10:30:00', + 'title' => 'Earth Day: small rituals', + 'content' => 'Planted two tomato starts and a wall of basil on the balcony. Every year I forget how satisfying it is to put your hands in dirt. Here is a quick list of what I learned from last year that I am doing differently.', + 'status' => 'publish', + 'cats' => array( 'garden' ), + ), + array( + 'date' => '2022-04-22 21:15:00', + 'title' => 'Late-night shipping log', + 'content' => 'Pushed v0.3 after a long weekend. Biggest change: the exporter no longer eats emoji. Smallest change: renamed a file I have hated for months. Both matter.', + 'status' => 'publish', + 'cats' => array( 'work', 'code' ), + ), + array( + 'date' => '2023-04-22 08:05:00', + 'title' => 'A quiet morning in Kyoto', + 'content' => 'The rain is soft and the maples are bright. Coffee at a six-seat counter, then a slow walk along the Kamo river. Travel note to self: do less, walk more.', + 'status' => 'publish', + 'cats' => array( 'travel' ), + ), + array( + 'date' => '2024-04-22 16:48:00', + 'title' => 'On rereading old posts', + 'content' => 'Went back through five years of this blog today. The embarrassing bits are fewer than I feared; the surprising joys are more than I remembered. Keep writing.', + 'status' => 'publish', + 'cats' => array( 'journal' ), + ), + array( + 'date' => '2024-04-22 22:11:00', + 'title' => 'Draft ideas I might finish someday', + 'content' => 'A private stash of titles, notes, and half-thoughts. Mostly nonsense, occasionally a spark.', + 'status' => 'private', + 'cats' => array(), + ), + array( + 'date' => '2025-04-22 11:22:00', + 'title' => 'Why I moved back to plain text', + 'content' => 'Fancy apps are lovely until they are not. Markdown files in a folder have outlasted every productivity tool I have tried. A short defense of the humble .md.', + 'status' => 'publish', + 'cats' => array( 'tools', 'writing' ), + ), +); + +$existing = get_posts( array( + 'post_status' => 'any', + 'posts_per_page' => -1, + 'meta_key' => '_otd_seed', + 'fields' => 'ids', + 'post_type' => 'post', +) ); +if ( $existing ) { + foreach ( $existing as $id ) { + wp_delete_post( $id, true ); + } + WP_CLI::log( 'Deleted ' . count( $existing ) . ' existing seed posts.' ); +} + +foreach ( $posts as $p ) { + $cat_ids = array(); + foreach ( $p['cats'] as $cat_name ) { + $term = term_exists( $cat_name, 'category' ); + if ( ! $term ) { + $term = wp_insert_term( ucfirst( $cat_name ), 'category' ); + } + if ( ! is_wp_error( $term ) && isset( $term['term_id'] ) ) { + $cat_ids[] = (int) $term['term_id']; + } + } + + $post_id = wp_insert_post( array( + 'post_author' => 1, + 'post_title' => $p['title'], + 'post_content' => $p['content'], + 'post_status' => $p['status'], + 'post_date' => $p['date'], + 'post_date_gmt' => get_gmt_from_date( $p['date'] ), + 'post_type' => 'post', + 'post_category' => $cat_ids, + 'meta_input' => array( '_otd_seed' => '1' ), + ), true ); + + if ( is_wp_error( $post_id ) ) { + WP_CLI::warning( 'Failed: ' . $p['title'] . ' – ' . $post_id->get_error_message() ); + } else { + WP_CLI::log( 'Created #' . $post_id . ' – ' . $p['date'] . ' – ' . $p['title'] ); + } +} From e1b0ba011ad46f9986ae9ae7560629c8ae2e115f Mon Sep 17 00:00:00 2001 From: Omar Alshaker Date: Wed, 22 Apr 2026 22:42:34 +0200 Subject: [PATCH 02/18] Lint --- .../includes/class-wp-on-this-day.php | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/wp-admin/includes/class-wp-on-this-day.php b/src/wp-admin/includes/class-wp-on-this-day.php index 24f565311dd4f..0c574dc490a12 100644 --- a/src/wp-admin/includes/class-wp-on-this-day.php +++ b/src/wp-admin/includes/class-wp-on-this-day.php @@ -157,7 +157,7 @@ protected static function render_posts( $posts ) { $by_year = array(); foreach ( $posts as $post ) { - $year = (int) get_the_date( 'Y', $post ); + $year = (int) get_the_date( 'Y', $post ); $by_year[ $year ][] = $post; } krsort( $by_year ); @@ -168,12 +168,14 @@ protected static function render_posts( $posts ) {

    ' . esc_html( number_format_i18n( $post_count ) ) . '', '' . esc_html( number_format_i18n( $year_count ) ) . '' ); @@ -181,7 +183,8 @@ protected static function render_posts( $posts ) {