Skip to content

Commit

Permalink
New block: Favorite button (#635)
Browse files Browse the repository at this point in the history
* New block: Favorite button

* Update the fallback API error

* Remove variant check for rendering

* Determine icon visibility with interactivity API binding
  • Loading branch information
ryelle authored Jul 16, 2024
1 parent 14b908d commit c944d1e
Show file tree
Hide file tree
Showing 7 changed files with 431 additions and 0 deletions.
151 changes: 151 additions & 0 deletions mu-plugins/blocks/favorite-button/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php
/**
* Block Name: Favorite Button
* Description: A button to toggle favoriting on the current item.
*
* @package wporg
*/

namespace WordPressdotorg\MU_Plugins\Favorite_Button_Block;

defined( 'WPINC' ) || die();

add_action( 'init', __NAMESPACE__ . '\init' );
add_action( 'rest_api_init', __NAMESPACE__ . '\api_init' );

/**
* Register the block.
*/
function init() {
register_block_type( __DIR__ . '/build' );
}

/**
* Get the block settings, set up the filters.
*/
function get_block_settings( $post_id ) {
/**
* Get the settings for the favorite button, used in rendering and saving favorites.
*
* @param array $settings {
* Array of settings for the favorite button.
*
* The return value should use the following format.
*
* @type callable $add_callback Callback function to handle adding the current
* item to a user's favorites. The function will
* be passed the post ID and request object. It
* should return a WP_Error, true, or the updated
* count of favorite items.
* @type callable $delete_callback Callback function to handle removing the current
* item from a user's favorites. Same arguments and
* return value as the `add_callback`.
* @type int $count Number of times this item has been favorited.
* @type bool $is_favorite Check if the current item is favorited.
* }
* @param int $post_id The current post ID.
*/
$settings = apply_filters( 'wporg_favorite_button_settings', array(), $post_id );
if ( empty( $settings ) ) {
return false;
}

$settings = wp_parse_args(
$settings,
array(
'add_callback' => '__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-not-implemented',
// Users should never see this error, so we can leave it untranslated.
'The `add_callback` function is not set correctly. It should return a wp_error, integer, or true.',
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-not-implemented',
// Users should never see this error, so we can leave it untranslated.
'The `delete_callback` function is not set correctly. It should return a wp_error, integer, or true.',
array( 'status' => 500 )
);
}
104 changes: 104 additions & 0 deletions mu-plugins/blocks/favorite-button/render.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php
/**
* Output the favorite button block.
*
* @package wporg
*/

namespace WordPressdotorg\MU_Plugins\Favorite_Button_Block;

$settings = get_block_settings( $block->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 ) {
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 );

?>
<div
<?php echo get_block_wrapper_attributes( [ 'class' => $classes ] ); // phpcs:ignore ?>
data-wp-interactive="wporg/favorite-button"
data-wp-context="<?php echo esc_attr( $encoded_state ); ?>"
data-wp-class--is-favorite="context.isFavorite"
>
<?php if ( $user_id ) : ?>
<button
class="wporg-favorite-button__button"
disabled="disabled"
data-wp-bind--disabled="!context.id"
data-wp-on--click="actions.triggerAction"
>
<?php else : ?>
<span class="wporg-favorite-button__button">
<?php endif; ?>

<svg data-wp-bind--hidden="context.isFavorite" class="is-heart-filled" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" aria-hidden="true" focusable="false">
<path d="M12 19.5c-2.213-1.953-4.61-3.882-6.394-6.247-.503-.667-.855-1.28-1.056-1.84a5.06 5.06 0 0 1-.3-1.717c0-1.195.407-2.193 1.22-2.994C6.285 5.9 7.299 5.5 8.513 5.5c.672 0 1.312.14 1.919.42.607.28 1.13.674 1.569 1.182A4.588 4.588 0 0 1 15.488 5.5c1.214 0 2.228.4 3.041 1.202.814.8 1.221 1.799 1.221 2.994 0 .585-.1 1.157-.3 1.717-.2.56-.552 1.173-1.056 1.84-1.784 2.365-4.18 4.294-6.394 6.247Z"/>
</svg>
<svg data-wp-bind--hidden="!context.isFavorite" class="is-heart-outline" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" aria-hidden="true" focusable="false">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.967 6.21c.385.243.729.54 1.033.892A4.601 4.601 0 0 1 15.488 5.5c1.214 0 2.228.4 3.041 1.202.814.8 1.221 1.799 1.221 2.994 0 .585-.1 1.157-.3 1.717-.2.56-.552 1.173-1.056 1.84-1.5 1.988-3.431 3.667-5.323 5.312-.36.312-.718.623-1.071.935-.353-.312-.711-.623-1.07-.935-1.893-1.645-3.824-3.325-5.324-5.312-.503-.667-.855-1.28-1.056-1.84a5.06 5.06 0 0 1-.3-1.717c0-1.195.407-2.193 1.22-2.994C6.285 5.9 7.299 5.5 8.513 5.5c.672 0 1.312.14 1.919.42.186.086.365.183.536.29ZM12 17.507l.116-.1c1.892-1.647 3.684-3.207 5.08-5.057.447-.592.708-1.07.841-1.443.143-.4.213-.8.213-1.21 0-.815-.26-1.422-.773-1.926-.514-.507-1.142-.771-1.99-.771a3.088 3.088 0 0 0-2.352 1.082L12 9.397l-1.135-1.315A3.088 3.088 0 0 0 8.512 7c-.847 0-1.475.264-1.99.77-.512.505-.772 1.112-.772 1.926 0 .41.07.812.213 1.211.133.372.394.851.84 1.443 1.397 1.85 3.189 3.41 5.081 5.056l.116.101Z"/>
</svg>

<?php if ( $show_count ) : ?>
<span class="wporg-favorite-button__count">
<span class="screen-reader-text" data-wp-text="state.labelScreenReader">
<?php echo esc_html( $sr_label ); ?>
</span>
<span aria-hidden="true" data-wp-text="state.labelCount"><?php echo esc_html( $settings['count'] ); ?></span>
</span>
<?php endif; ?>

<?php if ( $user_id ) : ?>
<span class="wporg-favorite-button__label screen-reader-text" data-wp-text="state.labelAction">
<?php echo esc_html( $is_favorite ? $labels['remove'] : $labels['add'] ); ?>
</span>
<?php endif; ?>

<?php if ( $user_id ) : ?>
</button>
<?php else : ?>
</span>
<?php endif; ?>
</div>
30 changes: 30 additions & 0 deletions mu-plugins/blocks/favorite-button/src/block.json
Original file line number Diff line number Diff line change
@@ -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"
}
25 changes: 25 additions & 0 deletions mu-plugins/blocks/favorite-button/src/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<div { ...useBlockProps() }>
<ServerSideRender block={ name } attributes={ attributes } skipBlockSupportAttributes />
</div>
);
}

registerBlockType( metadata.name, {
edit: Edit,
save: () => null,
} );
67 changes: 67 additions & 0 deletions mu-plugins/blocks/favorite-button/src/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
: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);
}

&[hidden] {
display: none;
}
}

&.is-variant-small {
.wporg-favorite-button__button {
border: none;
padding: 2px 4px;
}
}
}
Loading

0 comments on commit c944d1e

Please sign in to comment.