Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 152 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
<?php if ( is_user_logged_in() ) : ?>
<button
data-wpfav-post-id="<?php the_ID(); ?>"
aria-pressed="false"
class="wpfav-btn"
>
Favoritar
</button>
<?php endif; ?>
```

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.
109 changes: 109 additions & 0 deletions wp-favorites-plugin/assets/js/favorites.js
Original file line number Diff line number Diff line change
@@ -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<void>}
*/
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:
* <button data-wpfav-post-id="<?php the_ID(); ?>" aria-pressed="false">
* Favoritar
* </button>
*/
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;
} )();
94 changes: 94 additions & 0 deletions wp-favorites-plugin/includes/class-api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php
/**
* Registro e callbacks dos endpoints REST do plugin.
*
* @package WPFavoritesPlugin
*/

if ( ! defined( 'ABSPATH' ) ) {
exit;
}

class WPFAV_API {

/**
* Registra os endpoints da REST API.
*
* @return void
*/
public static function register_routes(): void {
register_rest_route(
'wpfav/v1',
'/toggle',
[
'methods' => 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 ]
);
}
}
Loading