diff --git a/README.md b/README.md index a4f6c256..dc826039 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,25 @@ Desenvolva um Plugin em WordPress que implemente a funcionalidade de favoritar p * PHP >= 5.6 * Orientado a objetos -## Dúvidas - -Em caso de dúvidas, crie uma issue. +## Como Usar o Plugin + +### 1. Instalação +- Faça upload do plugin para `/wp-content/plugins/` +- Ative o plugin no painel administrativo + +### 2. Adicionar Botão de Favorito +No arquivo `single.php` do seu tema, adicione: +```php + +``` + +### 3. Exibir Lista de Favoritos +Use o shortcode em qualquer página: +``` +[wp_favorites_list] +``` + +### 4. API REST +- **Favoritar**: `POST /wp-json/wp-favorites/v1/favorite` +- **Desfavoritar**: `POST /wp-json/wp-favorites/v1/unfavorite` +- **Listar**: `GET /wp-json/wp-favorites/v1/favorites` diff --git a/wp-favorites-plugin/assets/css/wp-favorites.css b/wp-favorites-plugin/assets/css/wp-favorites.css new file mode 100644 index 00000000..4e00fb50 --- /dev/null +++ b/wp-favorites-plugin/assets/css/wp-favorites.css @@ -0,0 +1,117 @@ +/** + * WP Favorites Plugin Styles + */ + +.wp-favorites-button { + display: inline-flex; + align-items: center; + gap: 5px; + background: #0073aa; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + transition: all 0.3s ease; + font-size: 14px; + text-decoration: none; + line-height: 1.4; +} + +.wp-favorites-button:hover { + background: #005a87; + color: white; + text-decoration: none; +} + +.wp-favorites-button.favorited { + background: #d63638; +} + +.wp-favorites-button.favorited:hover { + background: #b32d2e; +} + +.wp-favorites-button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.wp-favorites-icon { + font-size: 16px; + line-height: 1; +} + +.wp-favorites-count { + background: rgba(255, 255, 255, 0.2); + padding: 2px 6px; + border-radius: 10px; + font-size: 12px; + margin-left: 5px; +} + +.wp-favorites-message { + position: fixed; + top: 20px; + right: 20px; + padding: 12px 20px; + border-radius: 4px; + color: white; + z-index: 9999; + font-size: 14px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + animation: wp-favorites-slide-in 0.3s ease; +} + +.wp-favorites-message.success { + background: #46b450; +} + +.wp-favorites-message.error { + background: #dc3232; +} + +@keyframes wp-favorites-slide-in { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +/* Loading state */ +.wp-favorites-button.loading { + opacity: 0.7; + pointer-events: none; +} + +.wp-favorites-button.loading .wp-favorites-icon { + animation: wp-favorites-spin 1s linear infinite; +} + +@keyframes wp-favorites-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +/* Responsive */ +@media (max-width: 768px) { + .wp-favorites-button { + padding: 6px 12px; + font-size: 13px; + } + + .wp-favorites-message { + top: 10px; + right: 10px; + left: 10px; + font-size: 13px; + } +} \ No newline at end of file diff --git a/wp-favorites-plugin/assets/js/wp-favorites.js b/wp-favorites-plugin/assets/js/wp-favorites.js new file mode 100644 index 00000000..0f6ebbdb --- /dev/null +++ b/wp-favorites-plugin/assets/js/wp-favorites.js @@ -0,0 +1,194 @@ +/** + * WP Favorites Plugin JavaScript + */ + +(function($) { + 'use strict'; + + // WP Favorites Plugin namespace + window.WPFavorites = window.WPFavorites || {}; + + // Configuration + WPFavorites.config = { + restUrl: wpFavorites.restUrl || '/wp-json/wp-favorites/v1', + nonce: wpFavorites.nonce || '', + messages: { + favorited: 'Post added to favorites!', + unfavorited: 'Post removed from favorites!', + error: 'An error occurred. Please try again.', + loginRequired: 'Please log in to favorite posts.' + } + }; + + // Main class + WPFavorites.FavoritesButton = function(element, options) { + this.element = $(element); + this.options = $.extend({}, WPFavorites.config, options); + this.postId = this.element.data('post-id'); + this.isFavorited = false; + this.isLoading = false; + + this.init(); + }; + + WPFavorites.FavoritesButton.prototype = { + init: function() { + this.bindEvents(); + this.checkFavoriteStatus(); + }, + + bindEvents: function() { + var self = this; + this.element.on('click', function(e) { + e.preventDefault(); + self.toggleFavorite(); + }); + }, + + checkFavoriteStatus: function() { + var self = this; + $.ajax({ + url: self.options.restUrl + '/favorites', + method: 'GET', + beforeSend: function(xhr) { + xhr.setRequestHeader('X-WP-Nonce', self.options.nonce); + }, + success: function(response) { + if (response.success && response.favorites.includes(parseInt(self.postId))) { + self.setFavorited(true); + } + }, + error: function() { + // Silent fail for status check + } + }); + }, + + toggleFavorite: function() { + if (this.isLoading) return; + + if (this.isFavorited) { + this.unfavorite(); + } else { + this.favorite(); + } + }, + + favorite: function() { + var self = this; + this.setLoading(true); + + $.ajax({ + url: self.options.restUrl + '/favorite', + method: 'POST', + data: JSON.stringify({ + post_id: self.postId + }), + contentType: 'application/json', + beforeSend: function(xhr) { + xhr.setRequestHeader('X-WP-Nonce', self.options.nonce); + }, + success: function(response) { + if (response.success) { + self.setFavorited(true); + self.showMessage(self.options.messages.favorited, 'success'); + } else { + self.showMessage(response.message || self.options.messages.error, 'error'); + } + }, + error: function(xhr) { + var message = self.options.messages.error; + if (xhr.status === 401) { + message = self.options.messages.loginRequired; + } + self.showMessage(message, 'error'); + }, + complete: function() { + self.setLoading(false); + } + }); + }, + + unfavorite: function() { + var self = this; + this.setLoading(true); + + $.ajax({ + url: self.options.restUrl + '/unfavorite', + method: 'POST', + data: JSON.stringify({ + post_id: self.postId + }), + contentType: 'application/json', + beforeSend: function(xhr) { + xhr.setRequestHeader('X-WP-Nonce', self.options.nonce); + }, + success: function(response) { + if (response.success) { + self.setFavorited(false); + self.showMessage(self.options.messages.unfavorited, 'success'); + } else { + self.showMessage(response.message || self.options.messages.error, 'error'); + } + }, + error: function() { + self.showMessage(self.options.messages.error, 'error'); + }, + complete: function() { + self.setLoading(false); + } + }); + }, + + setFavorited: function(favorited) { + this.isFavorited = favorited; + this.element.toggleClass('favorited', favorited); + + var icon = this.element.find('.wp-favorites-icon'); + var text = this.element.find('.wp-favorites-text'); + + if (favorited) { + icon.text('♥'); + text.text('Favorited'); + } else { + icon.text('♡'); + text.text('Favorite'); + } + }, + + setLoading: function(loading) { + this.isLoading = loading; + this.element.toggleClass('loading', loading); + this.element.prop('disabled', loading); + }, + + showMessage: function(message, type) { + var messageElement = $('
') + .addClass('wp-favorites-message ' + type) + .text(message); + + $('body').append(messageElement); + + setTimeout(function() { + messageElement.fadeOut(300, function() { + $(this).remove(); + }); + }, 3000); + } + }; + + // jQuery plugin + $.fn.wpFavorites = function(options) { + return this.each(function() { + if (!$(this).data('wp-favorites')) { + $(this).data('wp-favorites', new WPFavorites.FavoritesButton(this, options)); + } + }); + }; + + // Auto-initialize on document ready + $(document).ready(function() { + $('.wp-favorites-button').wpFavorites(); + }); + +})(jQuery); \ No newline at end of file diff --git a/wp-favorites-plugin/config.php b/wp-favorites-plugin/config.php new file mode 100644 index 00000000..e448a098 --- /dev/null +++ b/wp-favorites-plugin/config.php @@ -0,0 +1,62 @@ + '1.0.0', + 'name' => 'WP Favorites Plugin', + 'description' => 'A WordPress plugin that allows logged-in users to favorite and unfavorite posts using WP REST API', + 'author' => 'WordPress Back-end Challenge', + 'license' => 'GPL v2 or later', + 'text_domain' => 'wp-favorites-plugin', + 'domain_path' => '/languages', + + // Database + 'table_name' => 'user_favorites', + + // REST API + 'rest_namespace' => 'wp-favorites/v1', + 'rest_routes' => array( + 'favorite' => '/favorite', + 'unfavorite' => '/unfavorite', + 'favorites' => '/favorites' + ), + + // Assets + 'assets' => array( + 'css' => array( + 'wp-favorites-style' => 'assets/css/wp-favorites.css' + ), + 'js' => array( + 'wp-favorites-script' => 'assets/js/wp-favorites.js' + ) + ), + + // Shortcodes + 'shortcodes' => array( + 'wp_favorites_button' => 'Favorite Button', + 'wp_favorites_list' => 'Favorites List' + ), + + // Hooks + 'hooks' => array( + 'wp_favorites_post_favorited' => 'Fired when a post is favorited', + 'wp_favorites_post_unfavorited' => 'Fired when a post is unfavorited' + ), + + // Requirements + 'requirements' => array( + 'php' => '5.6', + 'wordpress' => '4.7', + 'plugins' => array() + ) +); \ No newline at end of file diff --git a/wp-favorites-plugin/includes/class-wp-favorites-assets.php b/wp-favorites-plugin/includes/class-wp-favorites-assets.php new file mode 100644 index 00000000..d35f2744 --- /dev/null +++ b/wp-favorites-plugin/includes/class-wp-favorites-assets.php @@ -0,0 +1,106 @@ + rest_url('wp-favorites/v1'), + 'nonce' => wp_create_nonce('wp_rest'), + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'isLoggedIn' => is_user_logged_in(), + 'strings' => array( + 'favorited' => __('Post added to favorites!', 'wp-favorites-plugin'), + 'unfavorited' => __('Post removed from favorites!', 'wp-favorites-plugin'), + 'error' => __('An error occurred. Please try again.', 'wp-favorites-plugin'), + 'loginRequired' => __('Please log in to favorite posts.', 'wp-favorites-plugin') + ) + )); + } + + /** + * Enqueue admin scripts and styles + */ + public static function enqueue_admin_scripts($hook) { + // Only enqueue on plugin settings page + if (strpos($hook, 'wp-favorites') === false) { + return; + } + + wp_enqueue_style( + 'wp-favorites-admin-style', + WP_FAVORITES_PLUGIN_URL . 'assets/css/wp-favorites-admin.css', + array(), + WP_FAVORITES_PLUGIN_VERSION + ); + + wp_enqueue_script( + 'wp-favorites-admin-script', + WP_FAVORITES_PLUGIN_URL . 'assets/js/wp-favorites-admin.js', + array('jquery'), + WP_FAVORITES_PLUGIN_VERSION, + true + ); + } + + /** + * Get asset URL + * + * @param string $path Asset path + * @return string + */ + public static function get_asset_url($path) { + return WP_FAVORITES_PLUGIN_URL . 'assets/' . ltrim($path, '/'); + } + + /** + * Get asset path + * + * @param string $path Asset path + * @return string + */ + public static function get_asset_path($path) { + return WP_FAVORITES_PLUGIN_PATH . 'assets/' . ltrim($path, '/'); + } +} \ No newline at end of file diff --git a/wp-favorites-plugin/includes/class-wp-favorites-helpers.php b/wp-favorites-plugin/includes/class-wp-favorites-helpers.php new file mode 100644 index 00000000..30d4beac --- /dev/null +++ b/wp-favorites-plugin/includes/class-wp-favorites-helpers.php @@ -0,0 +1,124 @@ +prefix . 'user_favorites'; + $user_id = get_current_user_id(); + + $result = $wpdb->get_var($wpdb->prepare( + "SELECT COUNT(*) FROM $table_name WHERE user_id = %d AND post_id = %d", + $user_id, + $post_id + )); + + return $result > 0; + } + + /** + * Get user's favorite posts + * + * @param int $user_id User ID (optional, defaults to current user) + * @return array + */ + public static function get_user_favorites($user_id = null) { + if (!$user_id) { + $user_id = get_current_user_id(); + } + + if (!$user_id) { + return array(); + } + + global $wpdb; + $table_name = $wpdb->prefix . 'user_favorites'; + + $favorites = $wpdb->get_col($wpdb->prepare( + "SELECT post_id FROM $table_name WHERE user_id = %d ORDER BY created_at DESC", + $user_id + )); + + return $favorites; + } + + /** + * Get favorite count for a post + * + * @param int $post_id Post ID + * @return int + */ + public static function get_favorite_count($post_id) { + global $wpdb; + $table_name = $wpdb->prefix . 'user_favorites'; + + $count = $wpdb->get_var($wpdb->prepare( + "SELECT COUNT(*) FROM $table_name WHERE post_id = %d", + $post_id + )); + + return (int) $count; + } + + /** + * Sanitize post ID + * + * @param mixed $post_id + * @return int|false + */ + public static function sanitize_post_id($post_id) { + $post_id = absint($post_id); + return $post_id > 0 ? $post_id : false; + } + + /** + * Validate post exists + * + * @param int $post_id Post ID + * @return bool + */ + public static function post_exists($post_id) { + return get_post($post_id) !== null; + } + + /** + * Get REST API base URL + * + * @return string + */ + public static function get_rest_base_url() { + return rest_url('wp-favorites/v1'); + } + + /** + * Get nonce for AJAX requests + * + * @return string + */ + public static function get_nonce() { + return wp_create_nonce('wp_rest'); + } +} \ No newline at end of file diff --git a/wp-favorites-plugin/includes/class-wp-favorites-plugin.php b/wp-favorites-plugin/includes/class-wp-favorites-plugin.php new file mode 100644 index 00000000..a7a58ec9 --- /dev/null +++ b/wp-favorites-plugin/includes/class-wp-favorites-plugin.php @@ -0,0 +1,298 @@ +init_hooks(); + } + + /** + * Initialize hooks + */ + private function init_hooks() { + add_action('init', array($this, 'init')); + register_activation_hook(WP_FAVORITES_PLUGIN_FILE, array($this, 'activate')); + register_deactivation_hook(WP_FAVORITES_PLUGIN_FILE, array($this, 'deactivate')); + } + + /** + * Initialize plugin + */ + public function init() { + $this->create_table(); + $this->register_rest_routes(); + $this->init_assets(); + $this->init_templates(); + } + + /** + * Plugin activation + */ + public function activate() { + $this->create_table(); + } + + /** + * Plugin deactivation + */ + public function deactivate() { + // Cleanup if needed + } + + /** + * Create custom table + */ + private function create_table() { + global $wpdb; + + $table_name = $wpdb->prefix . 'user_favorites'; + $charset_collate = $wpdb->get_charset_collate(); + + $sql = "CREATE TABLE $table_name ( + id mediumint(9) NOT NULL AUTO_INCREMENT, + user_id bigint(20) NOT NULL, + post_id bigint(20) NOT NULL, + created_at datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY user_post (user_id, post_id) + ) $charset_collate;"; + + require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); + dbDelta($sql); + + // Verify table was created + $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") == $table_name; + if (!$table_exists) { + error_log('WP Favorites Plugin: Failed to create table ' . $table_name); + } + } + + /** + * Check if table exists and create if not + */ + private function ensure_table_exists() { + global $wpdb; + $table_name = $wpdb->prefix . 'user_favorites'; + + $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") == $table_name; + if (!$table_exists) { + $this->create_table(); + } + } + + /** + * Register REST API routes + */ + private function register_rest_routes() { + add_action('rest_api_init', function () { + register_rest_route('wp-favorites/v1', '/favorite', array( + 'methods' => 'POST', + 'callback' => array($this, 'favorite_post'), + 'permission_callback' => array($this, 'check_user_logged_in'), + )); + + register_rest_route('wp-favorites/v1', '/unfavorite', array( + 'methods' => 'POST', + 'callback' => array($this, 'unfavorite_post'), + 'permission_callback' => array($this, 'check_user_logged_in'), + )); + + register_rest_route('wp-favorites/v1', '/favorites', array( + 'methods' => 'GET', + 'callback' => array($this, 'get_user_favorites'), + 'permission_callback' => array($this, 'check_user_logged_in'), + )); + }); + } + + /** + * Check if user is logged in + */ + public function check_user_logged_in() { + return is_user_logged_in(); + } + + /** + * Favorite a post + */ + public function favorite_post($request) { + $user_id = get_current_user_id(); + $post_id = $request->get_param('post_id'); + + if (!$post_id || !get_post($post_id)) { + return new WP_Error('invalid_post', 'Invalid post ID', array('status' => 400)); + } + + global $wpdb; + $table_name = $wpdb->prefix . 'user_favorites'; + + // Ensure table exists + $this->ensure_table_exists(); + + // Check if already favorited + $existing = $wpdb->get_var($wpdb->prepare( + "SELECT id FROM $table_name WHERE user_id = %d AND post_id = %d", + $user_id, + $post_id + )); + + if ($existing) { + return array( + 'success' => true, + 'message' => 'Post already favorited', + 'post_id' => $post_id + ); + } + + $result = $wpdb->insert( + $table_name, + array( + 'user_id' => $user_id, + 'post_id' => $post_id, + ), + array('%d', '%d') + ); + + if ($result === false) { + return new WP_Error('database_error', 'Failed to favorite post', array('status' => 500)); + } + + do_action('wp_favorites_post_favorited', $user_id, $post_id); + + return array( + 'success' => true, + 'message' => 'Post favorited successfully', + 'post_id' => $post_id + ); + } + + /** + * Unfavorite a post + */ + public function unfavorite_post($request) { + $user_id = get_current_user_id(); + $post_id = $request->get_param('post_id'); + + if (!$post_id) { + return new WP_Error('invalid_post', 'Invalid post ID', array('status' => 400)); + } + + global $wpdb; + $table_name = $wpdb->prefix . 'user_favorites'; + + // Ensure table exists + $this->ensure_table_exists(); + + // Check if already favorited + $existing = $wpdb->get_var($wpdb->prepare( + "SELECT id FROM $table_name WHERE user_id = %d AND post_id = %d", + $user_id, + $post_id + )); + + if (!$existing) { + return array( + 'success' => true, + 'message' => 'Post not favorited', + 'post_id' => $post_id + ); + } + + $result = $wpdb->delete( + $table_name, + array( + 'user_id' => $user_id, + 'post_id' => $post_id, + ), + array('%d', '%d') + ); + + if ($result === false) { + return new WP_Error('database_error', 'Failed to unfavorite post', array('status' => 500)); + } + + do_action('wp_favorites_post_unfavorited', $user_id, $post_id); + + return array( + 'success' => true, + 'message' => 'Post unfavorited successfully', + 'post_id' => $post_id + ); + } + + /** + * Get user favorites + */ + public function get_user_favorites($request) { + $user_id = get_current_user_id(); + + global $wpdb; + $table_name = $wpdb->prefix . 'user_favorites'; + + // Ensure table exists + $this->ensure_table_exists(); + + $favorites = $wpdb->get_col($wpdb->prepare( + "SELECT post_id FROM $table_name WHERE user_id = %d ORDER BY created_at DESC", + $user_id + )); + + return array( + 'success' => true, + 'favorites' => $favorites + ); + } + + /** + * Initialize assets + */ + private function init_assets() { + require_once WP_FAVORITES_PLUGIN_PATH . 'includes/class-wp-favorites-assets.php'; + WP_Favorites_Assets::init(); + } + + /** + * Initialize template functions + */ + private function init_templates() { + require_once WP_FAVORITES_PLUGIN_PATH . 'includes/class-wp-favorites-helpers.php'; + require_once WP_FAVORITES_PLUGIN_PATH . 'includes/class-wp-favorites-template.php'; + WP_Favorites_Template::init(); + } +} \ No newline at end of file diff --git a/wp-favorites-plugin/includes/class-wp-favorites-template.php b/wp-favorites-plugin/includes/class-wp-favorites-template.php new file mode 100644 index 00000000..a06e6eb3 --- /dev/null +++ b/wp-favorites-plugin/includes/class-wp-favorites-template.php @@ -0,0 +1,148 @@ + get_the_ID(), + 'show_count' => false, + 'button_text' => __('Favorite', 'wp-favorites-plugin') + ); + + $args = wp_parse_args($args, $defaults); + + // Load template + $template_path = WP_FAVORITES_PLUGIN_PATH . 'templates/favorite-button.php'; + if (file_exists($template_path)) { + include $template_path; + } + } + + /** + * Favorite button shortcode + * + * @param array $atts Shortcode attributes + * @return string + */ + public static function favorite_button_shortcode($atts) { + $atts = shortcode_atts(array( + 'post_id' => get_the_ID(), + 'show_count' => 'false', + 'button_text' => __('Favorite', 'wp-favorites-plugin') + ), $atts); + + $atts['show_count'] = filter_var($atts['show_count'], FILTER_VALIDATE_BOOLEAN); + + ob_start(); + self::favorite_button($atts); + return ob_get_clean(); + } + + /** + * Favorites list shortcode + * + * @param array $atts Shortcode attributes + * @return string + */ + public static function favorites_list_shortcode($atts) { + if (!is_user_logged_in()) { + return '

' . __('Please log in to view your favorites.', 'wp-favorites-plugin') . '

'; + } + + $atts = shortcode_atts(array( + 'user_id' => get_current_user_id(), + 'limit' => 10, + 'show_excerpt' => 'false', + 'show_date' => 'false' + ), $atts); + + $favorites = WP_Favorites_Helpers::get_user_favorites($atts['user_id']); + + if (empty($favorites)) { + return '

' . __('No favorite posts found.', 'wp-favorites-plugin') . '

'; + } + + // Limit results + $favorites = array_slice($favorites, 0, (int) $atts['limit']); + + $output = '
'; + $output .= '

' . __('Your Favorite Posts', 'wp-favorites-plugin') . '

'; + $output .= ''; + $output .= '
'; + + return $output; + } + + /** + * Get template path + * + * @param string $template_name Template name + * @return string + */ + public static function get_template_path($template_name) { + return WP_FAVORITES_PLUGIN_PATH . 'templates/' . $template_name . '.php'; + } + + /** + * Load template + * + * @param string $template_name Template name + * @param array $args Template arguments + */ + public static function load_template($template_name, $args = array()) { + $template_path = self::get_template_path($template_name); + + if (file_exists($template_path)) { + extract($args); + include $template_path; + } + } +} \ No newline at end of file diff --git a/wp-favorites-plugin/templates/favorite-button.php b/wp-favorites-plugin/templates/favorite-button.php new file mode 100644 index 00000000..d2be9339 --- /dev/null +++ b/wp-favorites-plugin/templates/favorite-button.php @@ -0,0 +1,55 @@ + + + + + + + + \ No newline at end of file diff --git a/wp-favorites-plugin/uninstall.php b/wp-favorites-plugin/uninstall.php new file mode 100644 index 00000000..9f5184e5 --- /dev/null +++ b/wp-favorites-plugin/uninstall.php @@ -0,0 +1,23 @@ +prefix . 'user_favorites'; +$wpdb->query("DROP TABLE IF EXISTS $table_name"); + +// Delete plugin options +delete_option('wp_favorites_version'); +delete_option('wp_favorites_settings'); + +// Clear any cached data that has been removed +wp_cache_flush(); \ No newline at end of file diff --git a/wp-favorites-plugin/wp-favorites-plugin.php b/wp-favorites-plugin/wp-favorites-plugin.php new file mode 100644 index 00000000..06c18547 --- /dev/null +++ b/wp-favorites-plugin/wp-favorites-plugin.php @@ -0,0 +1,48 @@ +