# User Engagement Skill

> **Domain:** Voting (upvotes/downvotes) and comment reporting systems

---

## Purpose

This skill covers user engagement features including vote toggle logic, vote counting, comment reporting, and engagement-related database operations for the Comments Press Zone plugin.

---

## Voting System Architecture

### Database Schema

**Table:** `wp_presszone_comments_likes`

| Column | Type | Purpose |
|--------|------|---------|
| `id` | bigint(20) | Primary key |
| `comment_id` | bigint(20) | FK to wp_comments |
| `user_id` | bigint(20) | FK to wp_users (0 for guests) |
| `type` | varchar(20) | 'upvote' or 'downvote' |
| `ip_address` | varchar(45) | For guest identification |
| `created_at` | datetime | Timestamp |

---

## Vote Toggle Logic

### Toggle Vote (Core Pattern)

```php
public function toggle_vote(int $comment_id, string $type): array {
    global $wpdb;
    $table = $wpdb->prefix . 'presszone_comments_likes';
    
    $user_id = get_current_user_id();
    $ip_address = $this->get_user_ip();
    
    // Validate type
    if (!in_array($type, ['upvote', 'downvote'], true)) {
        throw new \InvalidArgumentException('Invalid vote type');
    }
    
    // Check for existing vote
    $existing = $wpdb->get_row($wpdb->prepare(
        "SELECT id, type FROM $table 
         WHERE comment_id = %d AND (user_id = %d OR ip_address = %s)",
        $comment_id,
        $user_id,
        $ip_address
    ));
    
    if ($existing) {
        if ($existing->type === $type) {
            // Remove vote (toggle off)
            $wpdb->delete($table, ['id' => $existing->id], ['%d']);
            $action = 'removed';
        } else {
            // Change vote type (upvote → downvote or vice versa)
            $wpdb->update(
                $table,
                ['type' => $type],
                ['id' => $existing->id],
                ['%s'],
                ['%d']
            );
            $action = 'updated';
        }
    } else {
        // Add new vote
        $wpdb->insert($table, [
            'comment_id' => $comment_id,
            'user_id' => $user_id,
            'type' => $type,
            'ip_address' => $ip_address,
        ], ['%d', '%d', '%s', '%s']);
        $action = 'added';
    }
    
    // Get updated counts
    $counts = $this->get_vote_counts($comment_id);
    
    // Invalidate cache
    wp_cache_delete('comment_votes_' . $comment_id, 'presszone_comments');
    
    return [
        'action' => $action,
        'upvotes' => $counts['upvotes'],
        'downvotes' => $counts['downvotes'],
    ];
}
```

### Get Vote Counts

```php
public function get_vote_counts(int $comment_id): array {
    global $wpdb;
    $table = $wpdb->prefix . 'presszone_comments_likes';
    
    // Check cache first
    $cache_key = 'comment_votes_' . $comment_id;
    $cached = wp_cache_get($cache_key, 'presszone_comments');
    if ($cached !== false) {
        return $cached;
    }
    
    $upvotes = (int) $wpdb->get_var($wpdb->prepare(
        "SELECT COUNT(*) FROM $table WHERE comment_id = %d AND type = 'upvote'",
        $comment_id
    ));
    
    $downvotes = (int) $wpdb->get_var($wpdb->prepare(
        "SELECT COUNT(*) FROM $table WHERE comment_id = %d AND type = 'downvote'",
        $comment_id
    ));
    
    $counts = [
        'upvotes' => $upvotes,
        'downvotes' => $downvotes,
        'score' => $upvotes - $downvotes,
    ];
    
    // Cache result
    wp_cache_set($cache_key, $counts, 'presszone_comments');
    
    return $counts;
}
```

### Check User Vote

```php
public function get_user_vote(int $comment_id, int $user_id = 0): ?string {
    global $wpdb;
    $table = $wpdb->prefix . 'presszone_comments_likes';
    
    if ($user_id === 0) {
        $user_id = get_current_user_id();
    }
    
    $ip_address = $this->get_user_ip();
    
    $type = $wpdb->get_var($wpdb->prepare(
        "SELECT type FROM $table 
         WHERE comment_id = %d AND (user_id = %d OR ip_address = %s)
         LIMIT 1",
        $comment_id,
        $user_id,
        $ip_address
    ));
    
    return $type ?: null;
}
```

---

## AJAX Vote Handler

```php
public function handle_vote(): void {
    // Verify nonce
    $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
    if (!wp_verify_nonce($nonce, 'presszone_comments_nonce')) {
        wp_send_json_error(['message' => esc_html__('Security check failed.', 'comments-press-zone')]);
    }
    
    // Check settings
    $settings = get_option('presszone_comments_settings', []);
    if (empty($settings['enable_voting'])) {
        wp_send_json_error(['message' => esc_html__('Voting is disabled.', 'comments-press-zone')]);
    }
    
    // Sanitize input
    $comment_id = isset($_POST['comment_id']) ? absint($_POST['comment_id']) : 0;
    $type = isset($_POST['type']) ? sanitize_key($_POST['type']) : '';
    
    // Validate
    if ($comment_id === 0 || !in_array($type, ['upvote', 'downvote'], true)) {
        wp_send_json_error(['message' => esc_html__('Invalid data.', 'comments-press-zone')]);
    }
    
    // Check if comment exists
    $comment = get_comment($comment_id);
    if (!$comment) {
        wp_send_json_error(['message' => esc_html__('Comment not found.', 'comments-press-zone')]);
    }
    
    // Process vote
    try {
        $result = $this->toggle_vote($comment_id, $type);
        
        wp_send_json_success([
            'action' => $result['action'],
            'upvotes' => $result['upvotes'],
            'downvotes' => $result['downvotes'],
            'score' => $result['upvotes'] - $result['downvotes'],
        ]);
    } catch (\Exception $e) {
        wp_send_json_error(['message' => esc_html__('Vote failed.', 'comments-press-zone')]);
    }
}
```

---

## Frontend Vote UI Update

```javascript
async function handleVote(commentId, type) {
    const formData = new FormData();
    formData.append('action', 'presszone_comments_vote');
    formData.append('nonce', window.presszoneCommentsData.nonce);
    formData.append('comment_id', commentId);
    formData.append('type', type);
    
    try {
        const response = await fetch(window.presszoneCommentsData.ajaxUrl, {
            method: 'POST',
            body: formData,
            credentials: 'same-origin'
        });
        
        const result = await response.json();
        
        if (result.success) {
            updateVoteUI(commentId, result.data);
        } else {
            showToast(result.data.message, 'error');
        }
    } catch (error) {
        console.error('Vote error:', error);
    }
}

function updateVoteUI(commentId, data) {
    const container = document.querySelector(`[data-comment-id="${commentId}"]`);
    if (!container) return;
    
    // Update counts
    const upvoteCount = container.querySelector('.presszone-comments-upvote-count');
    const downvoteCount = container.querySelector('.presszone-comments-downvote-count');
    
    if (upvoteCount) upvoteCount.textContent = data.upvotes;
    if (downvoteCount) downvoteCount.textContent = data.downvotes;
    
    // Update button states
    const upvoteBtn = container.querySelector('.presszone-comments-upvote-btn');
    const downvoteBtn = container.querySelector('.presszone-comments-downvote-btn');
    
    upvoteBtn.classList.toggle('active', data.action !== 'removed' && data.type === 'upvote');
    downvoteBtn.classList.toggle('active', data.action !== 'removed' && data.type === 'downvote');
}
```

---

## Reporting System

### Database Schema

**Table:** `wp_presszone_comments_reports`

| Column | Type | Purpose |
|--------|------|---------|
| `id` | bigint(20) | Primary key |
| `comment_id` | bigint(20) | FK to wp_comments |
| `user_id` | bigint(20) | FK to wp_users |
| `reason` | text | Report reason |
| `status` | varchar(20) | 'open', 'reviewed', 'dismissed' |
| `created_at` | datetime | Timestamp |

### Report Comment

```php
public function report_comment(int $comment_id, string $reason): bool {
    global $wpdb;
    $table = $wpdb->prefix . 'presszone_comments_reports';
    
    $user_id = get_current_user_id();
    if ($user_id === 0) {
        return false; // Require login
    }
    
    // Check if user already reported this comment
    $existing = $wpdb->get_var($wpdb->prepare(
        "SELECT id FROM $table WHERE comment_id = %d AND user_id = %d",
        $comment_id,
        $user_id
    ));
    
    if ($existing) {
        return false; // Already reported
    }
    
    // Insert report
    $result = $wpdb->insert($table, [
        'comment_id' => $comment_id,
        'user_id' => $user_id,
        'reason' => $reason,
        'status' => 'open',
    ], ['%d', '%d', '%s', '%s']);
    
    return $result !== false;
}
```

### AJAX Report Handler

```php
public function handle_report(): void {
    check_ajax_referer('presszone_comments_nonce', 'nonce');
    
    if (!is_user_logged_in()) {
        wp_send_json_error(['message' => esc_html__('You must be logged in to report.', 'comments-press-zone')]);
    }
    
    $comment_id = isset($_POST['comment_id']) ? absint($_POST['comment_id']) : 0;
    $reason = isset($_POST['reason']) ? sanitize_textarea_field(wp_unslash($_POST['reason'])) : '';
    
    if ($comment_id === 0) {
        wp_send_json_error(['message' => esc_html__('Invalid comment.', 'comments-press-zone')]);
    }
    
    $result = $this->report_comment($comment_id, $reason);
    
    if ($result) {
        wp_send_json_success([
            'message' => esc_html__('Thank you for your report.', 'comments-press-zone'),
        ]);
    } else {
        wp_send_json_error([
            'message' => esc_html__('You have already reported this comment.', 'comments-press-zone'),
        ]);
    }
}
```

---

## IP Address Helper

```php
private function get_user_ip(): string {
    if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
        $ip = sanitize_text_field(wp_unslash($_SERVER['HTTP_CLIENT_IP']));
    } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
        $ip = sanitize_text_field(wp_unslash($_SERVER['HTTP_X_FORWARDED_FOR']));
    } else {
        $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
    }
    return $ip;
}
```

---

## Common Mistakes to Avoid

| Mistake | Fix |
|---------|-----|
| Not invalidating vote cache | Clear cache after vote change |
| Missing IP address for guests | Store IP for guest vote tracking |
| No duplicate report prevention | Check if user already reported |
| Not checking if voting enabled | Verify settings before processing |
| Missing comment existence check | Validate comment exists |

---

## Testing Checklist

- [ ] Vote toggle works (add → remove → add)
- [ ] Vote type change works (upvote → downvote)
- [ ] Guest votes tracked by IP address
- [ ] Vote counts accurate and cached
- [ ] Cache invalidated on vote change
- [ ] Duplicate reports prevented
- [ ] Report requires login
- [ ] All vote/report actions logged
