From 89b2c938b1ad82a3a4301e863218b631b323cf79 Mon Sep 17 00:00:00 2001 From: Kelly Dwan Date: Tue, 2 Jul 2024 16:04:58 -0400 Subject: [PATCH] New block: Favorite button --- mu-plugins/blocks/favorite-button/index.php | 151 ++++++++++++++++++ mu-plugins/blocks/favorite-button/render.php | 104 ++++++++++++ .../blocks/favorite-button/src/block.json | 30 ++++ .../blocks/favorite-button/src/index.js | 25 +++ .../blocks/favorite-button/src/style.scss | 81 ++++++++++ mu-plugins/blocks/favorite-button/src/view.js | 53 ++++++ mu-plugins/loader.php | 1 + 7 files changed, 445 insertions(+) create mode 100644 mu-plugins/blocks/favorite-button/index.php create mode 100644 mu-plugins/blocks/favorite-button/render.php create mode 100644 mu-plugins/blocks/favorite-button/src/block.json create mode 100644 mu-plugins/blocks/favorite-button/src/index.js create mode 100644 mu-plugins/blocks/favorite-button/src/style.scss create mode 100644 mu-plugins/blocks/favorite-button/src/view.js diff --git a/mu-plugins/blocks/favorite-button/index.php b/mu-plugins/blocks/favorite-button/index.php new file mode 100644 index 000000000..e029d264d --- /dev/null +++ b/mu-plugins/blocks/favorite-button/index.php @@ -0,0 +1,151 @@ + '__return_false', + 'delete_callback' => '__return_false', + 'count' => 0, + 'is_favorite' => false, + ) + ); + + return $settings; +} + +/** + * Initialize the API endpoints. + */ +function api_init() { + $namespace = 'wporg/v1'; + $args = array( + 'id' => array( + 'validate_callback' => function( $param, $request, $key ) { + return is_numeric( $param ); + }, + 'required' => true, + ), + ); + register_rest_route( + $namespace, + '/favorite', + array( + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => __NAMESPACE__ . '\add_favorite', + 'args' => $args, + 'permission_callback' => 'is_user_logged_in', + ) + ); + register_rest_route( + $namespace, + '/favorite', + array( + 'methods' => \WP_REST_Server::DELETABLE, + 'callback' => __NAMESPACE__ . '\delete_favorite', + 'args' => $args, + 'permission_callback' => 'is_user_logged_in', + ) + ); +} + +/** + * Set the favorite status for a given item. + */ +function add_favorite( $request ) { + $id = intval( $request['id'] ); + $settings = get_block_settings( $id ); + $result = call_user_func( $settings['add_callback'], $id, $request ); + + if ( is_wp_error( $result ) ) { + return $result; + } else if ( false !== $result ) { + if ( is_numeric( $result ) ) { + return new \WP_REST_Response( $result, 200 ); + } else { + return new \WP_REST_Response( [ 'success' => true ] ); + } + } + + return new \WP_Error( + 'favorite-failed', + // Users should never see this error, so we can leave it untranslated. + 'Unable to favorite this item.', + array( 'status' => 500 ) + ); +} + +/** + * Remove the favorite status for a given item. + */ +function delete_favorite( $request ) { + $id = intval( $request['id'] ); + $settings = get_block_settings( $id ); + $result = call_user_func( $settings['delete_callback'], $id, $request ); + + if ( is_wp_error( $result ) ) { + return $result; + } else if ( false !== $result ) { + if ( is_numeric( $result ) ) { + return new \WP_REST_Response( $result, 200 ); + } else { + return new \WP_REST_Response( [ 'success' => true ] ); + } + } + + return new \WP_Error( + 'unfavorite-failed', + // Users should never see this error, so we can leave it untranslated. + 'Unable to remove this item from favorites.', + array( 'status' => 500 ) + ); +} diff --git a/mu-plugins/blocks/favorite-button/render.php b/mu-plugins/blocks/favorite-button/render.php new file mode 100644 index 000000000..50922d797 --- /dev/null +++ b/mu-plugins/blocks/favorite-button/render.php @@ -0,0 +1,104 @@ +context['postId'] ); +if ( ! $settings ) { + return ''; +} + +$user_id = get_current_user_id(); +$show_count = $attributes['showCount'] ?? false; +$variant = $attributes['variant'] ?? 'default'; + +if ( ! $user_id && ! $show_count && 'small' !== $variant ) { + return ''; +} + +// Manually enqueue this script, so that it's available for the interactivity view script. +wp_enqueue_script( 'wp-api-fetch' ); +wp_enqueue_script( 'wp-a11y' ); + +$is_favorite = $settings['is_favorite']; + +$classes = array( + $is_favorite ? 'is-favorite' : '', + ( 'small' === $variant ) ? 'is-variant-small' : '', +); +$classes = implode( ' ', array_filter( $classes ) ); + +$labels = array( + 'add' => __( 'Add to favorites', 'wporg' ), + 'remove' => __( 'Remove from favorites', 'wporg' ), + 'favorited' => __( 'Favorited', 'wporg' ), + 'unfavorited' => __( 'Removed from favorites', 'wporg' ), + // translators: %s: number of users who favorited this item. + 'screenReader' => __( 'Favorited %s times', 'wporg' ), +); + +$sr_label = sprintf( + // translators: %s: number of users who favorited this item. + _n( 'Favorited %s time', 'Favorited %s times', $settings['count'], 'wporg' ), + $settings['count'] +); + +// Initial state to pass to Interactivity API. +$init_state = [ + 'id' => $block->context['postId'], + 'count' => $settings['count'], + 'isFavorite' => $is_favorite, + 'label' => $labels, +]; +$encoded_state = wp_json_encode( $init_state ); + +?> +
$classes ] ); // phpcs:ignore ?> + data-wp-interactive="wporg/favorite-button" + data-wp-context="" + data-wp-class--is-favorite="context.isFavorite" +> + + + + + +
diff --git a/mu-plugins/blocks/favorite-button/src/block.json b/mu-plugins/blocks/favorite-button/src/block.json new file mode 100644 index 000000000..f06dd32d7 --- /dev/null +++ b/mu-plugins/blocks/favorite-button/src/block.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "wporg/favorite-button", + "version": "0.1.0", + "title": "Favorite Button", + "category": "design", + "icon": "", + "description": "A button to toggle favoriting on the current item.", + "textdomain": "wporg", + "supports": { + "html": false + }, + "attributes": { + "showCount": { + "type": "boolean", + "default": false + }, + "variant": { + "type": "string", + "enum": [ "default", "small" ], + "default": "default" + } + }, + "usesContext": [ "postId", "postType" ], + "editorScript": "file:./index.js", + "style": "file:./style-index.css", + "viewScriptModule": "file:./view.js", + "render": "file:../render.php" +} diff --git a/mu-plugins/blocks/favorite-button/src/index.js b/mu-plugins/blocks/favorite-button/src/index.js new file mode 100644 index 000000000..4f6215058 --- /dev/null +++ b/mu-plugins/blocks/favorite-button/src/index.js @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { registerBlockType } from '@wordpress/blocks'; +import ServerSideRender from '@wordpress/server-side-render'; +import { useBlockProps } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import './style.scss'; + +function Edit( { attributes, name } ) { + return ( +
+ +
+ ); +} + +registerBlockType( metadata.name, { + edit: Edit, + save: () => null, +} ); diff --git a/mu-plugins/blocks/favorite-button/src/style.scss b/mu-plugins/blocks/favorite-button/src/style.scss new file mode 100644 index 000000000..d8356ae1b --- /dev/null +++ b/mu-plugins/blocks/favorite-button/src/style.scss @@ -0,0 +1,81 @@ +:where(.wp-block-wporg-favorite-button) { + .wporg-favorite-button__button { + margin: 0; + padding: + var(--wp--custom--button--small--spacing--padding--top) + calc(var(--wp--custom--button--small--spacing--padding--right) - 4px) + var(--wp--custom--button--small--spacing--padding--bottom) + calc(var(--wp--custom--button--small--spacing--padding--left) - 4px); + background: none; + border: 1px solid var(--wp--preset--color--light-grey-1); + border-radius: 2px; + box-shadow: none; + font-size: 14px; + color: var(--wp--preset--color--charcoal-1); + + &:where(button) { + cursor: pointer; + + &:hover { + background-color: var(--wp--preset--color--light-grey-2); + } + + &:focus { + border-color: transparent; + } + + &:active { + border-color: transparent; + background-color: var(--wp--preset--color--charcoal-1); + color: var(--wp--preset--color--white); + + path { + fill: currentcolor; + } + } + } + } + + > * { + + /* Align children. */ + display: flex !important; + align-items: center; + gap: calc(var(--wp--preset--spacing--10) / 2); + } + + svg { + height: 24px; + width: 24px; + overflow: visible; + + path { + fill: var(--wp--preset--color--charcoal-4); + } + + &.is-heart-outline { + display: block; + } + + &.is-heart-filled { + display: none; + } + } + + &.is-favorite { + svg.is-heart-outline { + display: none; + } + + svg.is-heart-filled { + display: block; + } + } + + &.is-variant-small { + .wporg-favorite-button__button { + border: none; + padding: 2px 4px; + } + } +} diff --git a/mu-plugins/blocks/favorite-button/src/view.js b/mu-plugins/blocks/favorite-button/src/view.js new file mode 100644 index 000000000..0853cfd76 --- /dev/null +++ b/mu-plugins/blocks/favorite-button/src/view.js @@ -0,0 +1,53 @@ +/** + * WordPress dependencies + */ +import { getContext, store } from '@wordpress/interactivity'; + +store( 'wporg/favorite-button', { + state: { + get labelAction() { + const { label, isFavorite } = getContext(); + return isFavorite ? label.remove : label.add; + }, + get labelCount() { + const { count } = getContext(); + return `${ count }`; + }, + get labelScreenReader() { + const { label, count } = getContext(); + return label.screenReader.replace( '%s', count ); + }, + }, + actions: { + *triggerAction() { + const context = getContext(); + if ( context.isFavorite ) { + try { + const result = yield wp.apiFetch( { + path: '/wporg/v1/favorite', + method: 'DELETE', + data: { id: context.id }, + } ); + if ( 'number' === typeof result ) { + context.count = result; + } + context.isFavorite = false; + wp.a11y.speak( context.label.unfavorited, 'polite' ); + } catch ( error ) {} + } else { + try { + const result = yield wp.apiFetch( { + path: '/wporg/v1/favorite', + method: 'POST', + data: { id: context.id }, + } ); + if ( 'number' === typeof result ) { + context.count = result; + } + context.isFavorite = true; + wp.a11y.speak( context.label.favorited, 'polite' ); + } catch ( error ) {} + } + }, + }, +} ); diff --git a/mu-plugins/loader.php b/mu-plugins/loader.php index 486a31bc9..23b24e499 100644 --- a/mu-plugins/loader.php +++ b/mu-plugins/loader.php @@ -27,6 +27,7 @@ } require_once __DIR__ . '/helpers/helpers.php'; +require_once __DIR__ . '/blocks/favorite-button/index.php'; require_once __DIR__ . '/blocks/global-header-footer/blocks.php'; require_once __DIR__ . '/blocks/google-map/index.php'; require_once __DIR__ . '/blocks/handbook-meta-link/block.php';