diff --git a/README.md b/README.md
index a4f6c256..4d015d3d 100644
--- a/README.md
+++ b/README.md
@@ -1,27 +1,163 @@
-# WordPress Back-end Challenge
+# WP Favorites Plugin
-Desafio para os futuros programadores back-end em WordPress da Apiki.
+Plugin WordPress para favoritar posts via REST API, com persistência em tabela customizada.
-## Introdução
+## Funcionalidades
-Desenvolva um Plugin em WordPress que implemente a funcionalidade de favoritar posts para usuários logados usando a [WP REST API](https://developer.wordpress.org/rest-api/).
+- Favoritar e desfavoritar posts (toggle)
+- Listar favoritos do usuário logado
+- Persistência em tabela própria (`wp_favorites`)
+- Criação automática da tabela ao ativar o plugin
+- Sistema de versionamento da tabela para futuras atualizações
+- Script front-end com feedback visual e tratamento de erros
-**Especifícações**:
+## Requisitos
-* Possibilidade de favoritar e desfavoritar um post;
-* Persistir os dados em uma [tabela a parte](https://codex.wordpress.org/Creating_Tables_with_Plugins);
+- PHP >= 7.4
+- WordPress >= 5.0
-## Instruções
+## Instalação
-1. Efetue o fork deste repositório e crie um branch com o seu nome e sobrenome. (exemplo: fulano-dasilva)
-2. Após finalizar o desafio, crie um Pull Request.
-3. Aguarde algum contribuidor realizar o code review.
+1. Copie a pasta `wp-favorites-plugin` para `wp-content/plugins/`
+2. Ative o plugin no painel do WordPress
+3. A tabela `wp_favorites` será criada automaticamente
-## Pré-requisitos
+## Estrutura do projeto
-* PHP >= 5.6
-* Orientado a objetos
+```
+wp-favorites-plugin/
+├── wp-favorites-plugin.php # Bootstrap: constantes, hooks de ativação
+├── uninstall.php # Remove tabela e opções ao desinstalar
+├── includes/
+│ ├── class-database.php # Criação e atualização da tabela
+│ ├── class-plugin.php # Registro de hooks e enqueue de scripts
+│ ├── class-api.php # Endpoints REST (registro e callbacks)
+│ └── class-favorites.php # Regras de negócio (toggle, list)
+└── assets/
+ └── js/
+ └── favorites.js # Front-end: requisições e feedback visual
+```
-## Dúvidas
+## Endpoints REST
-Em caso de dúvidas, crie uma issue.
+Todos os endpoints exigem autenticação (usuário logado + nonce).
+
+### Toggle favorito
+
+```
+POST /wp-json/wpfav/v1/toggle
+```
+
+**Body (JSON):**
+```json
+{ "post_id": 123 }
+```
+
+**Resposta — adicionado (201):**
+```json
+{
+ "status": "added",
+ "post_id": 123,
+ "message": "Post adicionado aos favoritos."
+}
+```
+
+**Resposta — removido (200):**
+```json
+{
+ "status": "removed",
+ "post_id": 123,
+ "message": "Post removido dos favoritos."
+}
+```
+
+**Resposta — post não encontrado (404):**
+```json
+{
+ "code": "invalid_post",
+ "message": "O post informado não existe ou não é válido.",
+ "data": { "status": 404 }
+}
+```
+
+**Resposta — não autenticado (401):**
+```json
+{
+ "code": "rest_forbidden",
+ "message": "Você precisa estar logado para usar esta funcionalidade.",
+ "data": { "status": 401 }
+}
+```
+
+### Listar favoritos
+
+```
+GET /wp-json/wpfav/v1/list
+```
+
+**Resposta (200):**
+```json
+{
+ "total": 2,
+ "favorites": [
+ { "post_id": "123", "created_at": "2026-03-21 10:00:00" },
+ { "post_id": "45", "created_at": "2026-03-20 08:30:00" }
+ ]
+}
+```
+
+## Uso no tema (botão de favoritar)
+
+O plugin registra automaticamente o script `favorites.js` para usuários logados.
+Adicione o atributo `data-wpfav-post-id` a qualquer botão no seu tema:
+
+```php
+
+
+
+```
+
+O script escuta cliques via delegação de evento no `document`, portanto funciona
+com conteúdo carregado dinamicamente (infinite scroll, AJAX, etc.).
+
+### Exemplo de estilo CSS básico
+
+```css
+.wpfav-btn { cursor: pointer; }
+.wpfav-btn.wpfav--active { color: red; }
+.wpfav-btn[disabled] { opacity: 0.5; }
+```
+
+### Ouvindo o evento customizado
+
+```js
+document.addEventListener( 'wpfav:toggled', function ( event ) {
+ console.log( 'Post', event.detail.postId, '→', event.detail.status );
+} );
+```
+
+## Estrutura do banco de dados
+
+Tabela: `wp_favorites`
+
+| Coluna | Tipo | Descrição |
+|--------------|---------------------|-------------------------------|
+| `id` | BIGINT UNSIGNED PK | Identificador único |
+| `user_id` | BIGINT UNSIGNED | ID do usuário (FK wp_users) |
+| `post_id` | BIGINT UNSIGNED | ID do post (FK wp_posts) |
+| `created_at` | DATETIME | Data/hora de criação |
+
+Índices: `PRIMARY KEY (id)`, `UNIQUE KEY user_post (user_id, post_id)`, `KEY idx_user_id (user_id)`
+
+## Versionamento da tabela
+
+A versão da tabela é armazenada na opção `wpfav_db_version`.
+Ao atualizar o plugin, incremente `WPFAV_VERSION` em `wp-favorites-plugin.php`.
+O método `WPFAV_Database::maybe_update()` (hook `plugins_loaded`) detecta a diferença
+de versão e chama `create_table()` via `dbDelta()` automaticamente.
diff --git a/wp-favorites-plugin/assets/js/favorites.js b/wp-favorites-plugin/assets/js/favorites.js
new file mode 100644
index 00000000..a641456e
--- /dev/null
+++ b/wp-favorites-plugin/assets/js/favorites.js
@@ -0,0 +1,109 @@
+/**
+ * WP Favorites Plugin — front-end
+ *
+ * Usa wpFavSettings (definido via wp_localize_script) para obter:
+ * wpFavSettings.apiUrl — URL base da REST API do plugin
+ * wpFavSettings.nonce — nonce gerado pelo WordPress para autenticação REST
+ */
+
+( function () {
+ 'use strict';
+
+ /**
+ * Alterna o estado de favorito de um post.
+ *
+ * @param {number} postId - ID do post.
+ * @param {HTMLElement} [btn] - Botão que disparou a ação (opcional, para feedback visual).
+ * @returns {Promise}
+ */
+ async function toggleFavorite( postId, btn ) {
+ if ( ! postId || postId <= 0 ) {
+ console.error( 'wpfav: post_id inválido.', postId );
+ return;
+ }
+
+ // Feedback visual imediato: desabilita o botão durante a requisição.
+ if ( btn ) {
+ btn.disabled = true;
+ btn.setAttribute( 'aria-busy', 'true' );
+ }
+
+ try {
+ const response = await fetch( wpFavSettings.apiUrl + '/toggle', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-WP-Nonce': wpFavSettings.nonce,
+ },
+ body: JSON.stringify( { post_id: parseInt( postId, 10 ) } ),
+ } );
+
+ // Trata erros HTTP (401, 404, 500, etc.)
+ if ( ! response.ok ) {
+ const errorData = await response.json().catch( () => ( {} ) );
+ const message = errorData.message || `Erro HTTP ${ response.status }`;
+ console.error( 'wpfav toggle error:', message );
+
+ if ( btn ) {
+ btn.setAttribute( 'data-wpfav-error', message );
+ }
+
+ return;
+ }
+
+ const data = await response.json();
+
+ // Atualiza o estado visual do botão conforme a resposta da API.
+ if ( btn ) {
+ const isFavorited = data.status === 'added';
+ btn.classList.toggle( 'wpfav--active', isFavorited );
+ btn.setAttribute( 'aria-pressed', String( isFavorited ) );
+ btn.setAttribute( 'title', isFavorited ? 'Remover dos favoritos' : 'Adicionar aos favoritos' );
+ btn.removeAttribute( 'data-wpfav-error' );
+ }
+
+ // Dispara um evento customizado para que o tema/outros scripts possam reagir.
+ document.dispatchEvent(
+ new CustomEvent( 'wpfav:toggled', {
+ detail: { postId: data.post_id, status: data.status },
+ } )
+ );
+ } catch ( error ) {
+ // Trata falhas de rede (sem conexão, timeout, etc.)
+ console.error( 'wpfav: falha na requisição.', error );
+
+ if ( btn ) {
+ btn.setAttribute( 'data-wpfav-error', 'Falha na conexão. Tente novamente.' );
+ }
+ } finally {
+ // Reabilita o botão independentemente do resultado.
+ if ( btn ) {
+ btn.disabled = false;
+ btn.removeAttribute( 'aria-busy' );
+ }
+ }
+ }
+
+ /**
+ * Delega o clique em botões com data-wpfav-post-id dentro do document.
+ * Compatível com conteúdo carregado dinamicamente (infinite scroll, etc.).
+ *
+ * Uso no tema:
+ *
+ */
+ document.addEventListener( 'click', function ( event ) {
+ const btn = event.target.closest( '[data-wpfav-post-id]' );
+
+ if ( ! btn ) {
+ return;
+ }
+
+ const postId = parseInt( btn.dataset.wpfavPostId, 10 );
+ toggleFavorite( postId, btn );
+ } );
+
+ // Expõe a função globalmente para uso programático se necessário.
+ window.wpFavToggle = toggleFavorite;
+} )();
diff --git a/wp-favorites-plugin/includes/class-api.php b/wp-favorites-plugin/includes/class-api.php
new file mode 100644
index 00000000..e716bdc7
--- /dev/null
+++ b/wp-favorites-plugin/includes/class-api.php
@@ -0,0 +1,94 @@
+ WP_REST_Server::CREATABLE,
+ 'callback' => [ self::class, 'toggle' ],
+ 'permission_callback' => [ self::class, 'user_is_logged_in' ],
+ 'args' => [
+ 'post_id' => [
+ 'required' => true,
+ 'type' => 'integer',
+ 'minimum' => 1,
+ 'sanitize_callback' => 'absint',
+ 'validate_callback' => static function ( $value ) {
+ return is_numeric( $value ) && (int) $value > 0;
+ },
+ 'description' => __( 'ID do post a favoritar/desfavoritar.', 'wp-favorites-plugin' ),
+ ],
+ ],
+ ]
+ );
+
+ register_rest_route(
+ 'wpfav/v1',
+ '/list',
+ [
+ 'methods' => WP_REST_Server::READABLE, // 'GET'
+ 'callback' => [ self::class, 'get_favorites' ],
+ 'permission_callback' => [ self::class, 'user_is_logged_in' ],
+ ]
+ );
+ }
+
+ /**
+ * Callback do endpoint POST /toggle.
+ *
+ * @param WP_REST_Request $request Objeto da requisição.
+ *
+ * @return WP_REST_Response|WP_Error
+ */
+ public static function toggle( WP_REST_Request $request ) {
+ $user_id = get_current_user_id();
+ $post_id = (int) $request->get_param( 'post_id' );
+
+ return WPFAV_Favorites::toggle_favorite( $user_id, $post_id );
+ }
+
+ /**
+ * Callback do endpoint GET /list.
+ *
+ * @return WP_REST_Response
+ */
+ public static function get_favorites(): WP_REST_Response {
+ $user_id = get_current_user_id();
+
+ return WPFAV_Favorites::get_user_favorites( $user_id );
+ }
+
+ /**
+ * Permission callback reutilizável: verifica se o usuário está logado.
+ *
+ * @return bool|WP_Error
+ */
+ public static function user_is_logged_in() {
+ if ( is_user_logged_in() ) {
+ return true;
+ }
+
+ return new WP_Error(
+ 'rest_forbidden',
+ __( 'Você precisa estar logado para usar esta funcionalidade.', 'wp-favorites-plugin' ),
+ [ 'status' => 401 ]
+ );
+ }
+}
diff --git a/wp-favorites-plugin/includes/class-database.php b/wp-favorites-plugin/includes/class-database.php
new file mode 100644
index 00000000..88f10e27
--- /dev/null
+++ b/wp-favorites-plugin/includes/class-database.php
@@ -0,0 +1,62 @@
+prefix . 'favorites';
+ $charset_collate = $wpdb->get_charset_collate();
+
+ /*
+ * IMPORTANTE: dbDelta() é sensível ao formato do SQL.
+ * Referência: https://developer.wordpress.org/plugins/creating-tables-with-plugins/
+ */
+ $sql = "CREATE TABLE {$table_name} (
+ id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
+ user_id BIGINT(20) UNSIGNED NOT NULL,
+ post_id BIGINT(20) UNSIGNED NOT NULL,
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ UNIQUE KEY user_post (user_id, post_id),
+ KEY idx_user_id (user_id)
+ ) {$charset_collate};";
+
+ require_once ABSPATH . 'wp-admin/includes/upgrade.php';
+ dbDelta( $sql );
+
+ update_option( self::DB_VERSION_OPTION, WPFAV_VERSION );
+ }
+
+ /**
+ * Verifica se a versão instalada da tabela está desatualizada
+ * e executa create_table() caso necessário.
+ * Chamada no hook plugins_loaded para suportar atualizações.
+ *
+ * @return void
+ */
+ public static function maybe_update(): void {
+ $installed_version = get_option( self::DB_VERSION_OPTION, '0.0.0' );
+
+ if ( version_compare( $installed_version, WPFAV_VERSION, '<' ) ) {
+ self::create_table();
+ }
+ }
+}
diff --git a/wp-favorites-plugin/includes/class-favorites.php b/wp-favorites-plugin/includes/class-favorites.php
new file mode 100644
index 00000000..f50fba52
--- /dev/null
+++ b/wp-favorites-plugin/includes/class-favorites.php
@@ -0,0 +1,148 @@
+ 404 ]
+ );
+ }
+
+ global $wpdb;
+ $table = $wpdb->prefix . 'favorites';
+
+ // Verifica se o favorito já existe.
+ $exists = $wpdb->get_var(
+ $wpdb->prepare(
+ "SELECT id FROM {$table} WHERE user_id = %d AND post_id = %d LIMIT 1",
+ $user_id,
+ $post_id
+ )
+ );
+
+ if ( $exists ) {
+ $deleted = $wpdb->delete(
+ $table,
+ [
+ 'user_id' => $user_id,
+ 'post_id' => $post_id,
+ ],
+ [ '%d', '%d' ]
+ );
+
+ if ( false === $deleted ) {
+ return new WP_Error(
+ 'db_error',
+ __( 'Erro ao remover o favorito.', 'wp-favorites-plugin' ),
+ [ 'status' => 500 ]
+ );
+ }
+
+ return new WP_REST_Response(
+ [
+ 'status' => 'removed',
+ 'post_id' => $post_id,
+ 'message' => __( 'Post removido dos favoritos.', 'wp-favorites-plugin' ),
+ ],
+ 200
+ );
+ }
+
+ // Insere o novo favorito declarando os formatos de cada coluna.
+ $inserted = $wpdb->insert(
+ $table,
+ [
+ 'user_id' => $user_id,
+ 'post_id' => $post_id,
+ ],
+ [ '%d', '%d' ]
+ );
+
+ if ( false === $inserted ) {
+ return new WP_Error(
+ 'db_error',
+ __( 'Erro ao salvar o favorito.', 'wp-favorites-plugin' ),
+ [ 'status' => 500 ]
+ );
+ }
+
+ return new WP_REST_Response(
+ [
+ 'status' => 'added',
+ 'post_id' => $post_id,
+ 'message' => __( 'Post adicionado aos favoritos.', 'wp-favorites-plugin' ),
+ ],
+ 201
+ );
+ }
+
+ /**
+ * Retorna todos os posts favoritados por um usuário.
+ *
+ * @param int $user_id ID do usuário.
+ *
+ * @return WP_REST_Response
+ */
+ public static function get_user_favorites( int $user_id ): WP_REST_Response {
+ $user_id = absint( $user_id );
+
+ global $wpdb;
+ $table = $wpdb->prefix . 'favorites';
+
+ $results = $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT post_id, created_at FROM {$table} WHERE user_id = %d ORDER BY created_at DESC",
+ $user_id
+ )
+ );
+
+ return new WP_REST_Response(
+ [
+ 'total' => count( $results ),
+ 'favorites' => $results,
+ ],
+ 200
+ );
+ }
+
+ /**
+ * Verifica se um post_id é válido: existe, é do tipo 'post' e está publicado.
+ *
+ * @param int $post_id ID do post.
+ *
+ * @return bool
+ */
+ private static function post_is_valid( int $post_id ): bool {
+ if ( $post_id <= 0 ) {
+ return false;
+ }
+
+ $post = get_post( $post_id );
+
+ return ( $post instanceof WP_Post && 'post' === $post->post_type && 'publish' === $post->post_status );
+ }
+}
diff --git a/wp-favorites-plugin/includes/class-plugin.php b/wp-favorites-plugin/includes/class-plugin.php
new file mode 100644
index 00000000..7812fedb
--- /dev/null
+++ b/wp-favorites-plugin/includes/class-plugin.php
@@ -0,0 +1,68 @@
+load_dependencies();
+
+
+ add_action( 'plugins_loaded', [ 'WPFAV_Database', 'maybe_update' ] );
+ add_action( 'rest_api_init', [ 'WPFAV_API', 'register_routes' ] );
+ add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
+ }
+
+ /**
+ * Carrega os arquivos de classes necessários.
+ *
+ * @return void
+ */
+ private function load_dependencies(): void {
+ require_once WPFAV_PLUGIN_PATH . 'includes/class-api.php';
+ require_once WPFAV_PLUGIN_PATH . 'includes/class-favorites.php';
+ }
+
+ /**
+ * Registra e localiza o script JS do plugin.
+ *
+ * @return void
+ */
+ public function enqueue_scripts(): void {
+ if ( ! is_user_logged_in() ) {
+ return;
+ }
+
+ wp_enqueue_script(
+ 'wpfav-favorites',
+ WPFAV_PLUGIN_URL . 'assets/js/favorites.js',
+ [],
+ WPFAV_VERSION,
+ true
+ );
+
+ /*
+ * wp_localize_script: passa dados do PHP para o JS de forma segura.
+ */
+ wp_localize_script(
+ 'wpfav-favorites',
+ 'wpFavSettings',
+ [
+ 'apiUrl' => esc_url_raw( rest_url( 'wpfav/v1' ) ),
+ 'nonce' => wp_create_nonce( 'wp_rest' ),
+ ]
+ );
+ }
+}
diff --git a/wp-favorites-plugin/uninstall.php b/wp-favorites-plugin/uninstall.php
new file mode 100644
index 00000000..4e60ab8c
--- /dev/null
+++ b/wp-favorites-plugin/uninstall.php
@@ -0,0 +1,17 @@
+prefix . 'favorites';
+
+$wpdb->query( 'DROP TABLE IF EXISTS `' . esc_sql( $table ) . '`' );
+
+delete_option( 'wpfav_db_version' );
diff --git a/wp-favorites-plugin/wp-favorites-plugin.php b/wp-favorites-plugin/wp-favorites-plugin.php
new file mode 100644
index 00000000..f68a33c2
--- /dev/null
+++ b/wp-favorites-plugin/wp-favorites-plugin.php
@@ -0,0 +1,31 @@
+run();
+}
+
+wpfav_init();