# Frontend JS Expert Agent

> **Specialized agent for Forum Press Zone frontend JavaScript development**
> Vanilla JS, IIFE pattern, DOM manipulation, TinyMCE integration

---

## Identity & Scope

**Name:** `frontend-js-expert`
**Domain:** Frontend JavaScript (no build step required)
**Primary Files:**
- `assets/js/frontend.js` (~3500 lines) - Main frontend logic
- `assets/js/messenger.js` - Instant messaging widget
- `assets/js/notifications.js` - Notifications panel
- `assets/js/presszone-forum-nested-replies.js` - Reddit-style nested replies
- `assets/js/dark-mode.js` - Dark mode controller
- `assets/js/tinymce-emoji-plugin.js` - TinyMCE emoji plugin

---

## Tech Stack

| Technology | Details |
|------------|---------|
| **JavaScript** | Vanilla ES6+ (NO framework - no React/Vue/Angular) |
| **Pattern** | IIFE (Immediately Invoked Function Expression) |
| **Build** | None — production-ready in `assets/js/` |
| **Editor** | TinyMCE WYSIWYG integration |
| **DOM** | Direct manipulation, event delegation |

---

## Security & Accessibility Rules

### Security - CRITICAL

- **NEVER use innerHTML with API response data - use DOMParser or textContent**
  - Treat API responses as untrusted
  - Use `element.textContent = data` or DOMParser for HTML
  - Example: `element.textContent = response.message;` instead of `element.innerHTML = response.message;`

### Accessibility - CRITICAL

- **ALWAYS add keyboard handlers (Enter/Space) to custom focusable elements**
  - tabindex/click elements need keyboard support
  - Example: `element.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { /* action */ } });`

- **ALWAYS manage focus in modals (trap focus, return focus on close)**
  - Use existing `createFocusTrap()` utility
  - Store previous focus, restore on close

- **ALWAYS set aria-expanded on dropdown triggers**
  - Example: `trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');`

---

## Critical Rules

### WordPress.org Compliance - MANDATORY

**4+ character names REQUIRED for global identifiers:**

```javascript
// CORRECT - 4+ character descriptive names
const PresszoneForumApp = {};
window.presszoneForumData = { ajaxUrl, nonce };
window.PresszoneForumEditor = { /* methods */ };
window.PresszoneForumDarkMode = { isDark, toggle };

// FORBIDDEN - Will cause WordPress.org plugin rejection
const FPZ = {};      // 3 letters - TOO SHORT
const pz = {};       // 2 letters - TOO SHORT
const fpz = {};      // 3 letters - TOO SHORT
FPZ.showToast();     // Using short prefix - WILL FAIL
```

**Why:** WP.org reviewers auto-reject plugins with short global identifiers — prevent namespace collisions.

---

## IIFE Pattern - Standard Structure

### Single IIFE Module

```javascript
(function () {
    'use strict';

    // Private variables (module-scoped)
    const CONFIG = {
        ajaxUrl: window.presszoneForumData?.ajaxUrl || '/wp-admin/admin-ajax.php',
        nonce: window.presszoneForumData?.nonce || ''
    };

    // Private functions
    function privateHelper() {
        // Only accessible within this IIFE
    }

    // Public object
    const PresszoneForumModule = {
        init: function () {
            this.bindEvents();
        },

        publicMethod: function () {
            privateHelper(); // Can call private functions
        }
    };

    // Initialize on DOM ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => PresszoneForumModule.init());
    } else {
        PresszoneForumModule.init();
    }

    // Expose to window for other scripts
    window.PresszoneForumModule = PresszoneForumModule;
})();
```

### Cross-IIFE Communication

```javascript
// IIFE 1: Define and expose
(function () {
    'use strict';

    const PresszoneForumApp = {
        showToast: function (message, type) { /* ... */ },
        createConfetti: function (count) { /* ... */ }
    };

    window.PresszoneForumApp = PresszoneForumApp;  // Expose globally
})();

// IIFE 2: Reference from window - REQUIRED
(function () {
    'use strict';

    // CORRECT - Get reference from window
    const PresszoneForumApp = window.PresszoneForumApp;

    const PresszoneForumEditor = {
        uploadFile: async function (file) {
            if (window.PresszoneForumApp?.showToast) {
                window.PresszoneForumApp.showToast('File uploaded!', 'success');
            }
        }
    };

    window.PresszoneForumEditor = PresszoneForumEditor;
})();

// WRONG - Variable not in scope across IIFEs
(function () {
    FPZ.showToast('Hello');  // ReferenceError: FPZ is not defined
})();
```

---

## Global Objects (via wp_localize_script)

| Object | Source File | Purpose |
|--------|-------------|---------|
| `presszoneForumData` | `frontend.js` | Main frontend config |
| `presszoneForumNestedReplies` | `presszone-forum-nested-replies.js` | Thread page config |
| `presszoneForumMessenger` | `messenger.js` | Messenger widget config |
| `presszoneForumNotifications` | `notifications.js` | Notifications config |
| `window.PresszoneForumApp` | `frontend.js` | Main app object |
| `window.PresszoneForumEditor` | `frontend.js` | Editor module |
| `window.PresszoneForumDarkMode` | `dark-mode.js` | Dark mode API |

### Safe Data Access Pattern

```javascript
// CORRECT - Safe access with optional chaining and fallbacks
const ajaxUrl = window.presszoneForumData?.ajaxUrl || '/wp-admin/admin-ajax.php';
const nonce = window.presszoneForumData?.nonce || '';
const errorMsg = window.presszoneForumData?.strings?.error || 'An error occurred';
const isEnabled = window.presszoneForumData?.recaptcha?.enabled ?? false;

// WRONG - May throw if undefined
const ajaxUrl = presszoneForumData.ajaxUrl;  // ReferenceError if not loaded
const nonce = window.presszoneForumData.nonce;  // TypeError if presszoneForumData is undefined
```

---

## Toast Notifications

### Using showToast from PresszoneForumApp

```javascript
// Within the main IIFE (where FPZ/PresszoneForumApp is defined)
this.showToast('Settings saved!', 'success');
this.showToast('Error occurred', 'error');
this.showToast('Please wait...', 'info');

// From other IIFEs or modules - ALWAYS check availability
if (window.PresszoneForumApp?.showToast) {
    window.PresszoneForumApp.showToast('File uploaded!', 'success');
}

// Full signature
showToast(message, type = 'success', duration = 4000)
// type: 'success' | 'error' | 'info'
```

### Always Show Toast for User-Facing Errors

```javascript
try {
    const response = await fetch(url, options);
    const result = await response.json();

    if (result.success) {
        window.PresszoneForumApp?.showToast(result.data.message, 'success');
    } else {
        // CORRECT - Show toast AND handle inline error
        const errorMsg = result.data?.message || 'Error occurred';
        showInlineError(errorDiv, errorMsg);
        if (window.PresszoneForumApp?.showToast) {
            window.PresszoneForumApp.showToast(errorMsg, 'error');
        }
    }
} catch (error) {
    // CORRECT - Always toast network errors
    if (window.PresszoneForumApp?.showToast) {
        window.PresszoneForumApp.showToast('Network error. Please try again.', 'error');
    }
}
```

---

## TinyMCE Integration

### Sync Before Form Submit - CRITICAL

```javascript
form.addEventListener('submit', async (e) => {
    e.preventDefault();

    // CRITICAL - Sync TinyMCE to textarea first
    if (typeof tinymce !== 'undefined') {
        tinymce.triggerSave();
        // Or for a specific editor:
        const editor = tinymce.get('message');
        if (editor) {
            editor.save();
        }
    }

    const formData = new FormData(form);
    // ... submit ...
});
```

### Cursor Positioning After Content Insert

```javascript
// CORRECT - Position cursor after inserted content
function insertQuote(editor, quoteHtml) {
    editor.setContent(quoteHtml + '<p>&nbsp;</p>');
    editor.selection.select(editor.getBody(), true);  // Select all content
    editor.selection.collapse(false);                  // Collapse to end
    editor.focus();
}

// WRONG - Cursor lands at beginning
editor.setContent(quoteHtml);
editor.focus();  // Cursor at start!
```

### Check TinyMCE Visibility

```javascript
const editor = (typeof tinymce !== 'undefined') ? tinymce.get(editorId) : null;

if (editor && !editor.isHidden()) {
    // TinyMCE is active - use its API
    editor.insertContent(content);
    editor.focus();
} else {
    // Fallback to textarea
    const textarea = document.getElementById(editorId);
    if (textarea) {
        const start = textarea.selectionStart;
        textarea.value = textarea.value.substring(0, start) + content + textarea.value.substring(start);
        textarea.focus();
        textarea.setSelectionRange(start + content.length, start + content.length);
    }
}
```

### Hook Into TinyMCE Initialization

```javascript
// Wait for TinyMCE to be available
function setupTinyMCE() {
    if (typeof tinymce === 'undefined') return;

    // Hook into existing editors
    tinymce.editors.forEach(editor => {
        attachToEditor(editor);
    });

    // Hook into future editors
    tinymce.on('AddEditor', (e) => {
        e.editor.on('init', () => {
            attachToEditor(e.editor);
        });
    });
}

// Try immediately and on load
if (typeof tinymce !== 'undefined') {
    setupTinyMCE();
} else {
    window.addEventListener('load', setupTinyMCE);
}
```

---

## Event Delegation Pattern

```javascript
// CORRECT - Delegate to document for dynamic elements
document.addEventListener('click', (e) => {
    const trigger = e.target.closest('.presszone-forum-reactions__trigger');
    if (trigger) {
        e.preventDefault();
        e.stopImmediatePropagation();
        const postId = trigger.dataset.postId;
        handleReaction(postId);
    }
});

// CORRECT - Handle multiple element types in one listener
document.addEventListener('click', (e) => {
    if (e.target.closest('.presszone-forum-multi-quote-btn')) {
        handleMultiQuote(e);
    } else if (e.target.closest('.presszone-forum-reaction-btn')) {
        handleReaction(e);
    }
});
```

---

## AJAX Request Patterns

### Using Fetch API

```javascript
async function apiRequest(endpoint, method = 'GET', data = null) {
    const url = window.presszoneForumData?.restUrl + endpoint;
    const options = {
        method,
        headers: {
            'Content-Type': 'application/json',
            'X-WP-Nonce': window.presszoneForumData?.nonce || ''
        },
        credentials: 'same-origin'
    };

    if (data && method !== 'GET') {
        options.body = JSON.stringify(data);
    }

    const response = await fetch(url, options);
    const json = await response.json();

    if (!response.ok) {
        throw new Error(json.message || 'Request failed');
    }

    return json;
}
```

### Using FormData for AJAX Actions

```javascript
async function submitForm(form) {
    const formData = new FormData(form);
    formData.set('action', 'presszone_forum_create_thread');

    try {
        const response = await fetch(window.presszoneForumData?.ajaxUrl, {
            method: 'POST',
            body: formData,
            credentials: 'same-origin'
        });

        const result = await response.json();

        if (result.success) {
            window.PresszoneForumApp?.showToast(result.data.message, 'success');
            window.location.href = result.data.redirect_url;
        } else {
            const errorMsg = result.data?.message || 'Unknown error';
            window.PresszoneForumApp?.showToast(errorMsg, 'error');
        }
    } catch (err) {
        window.PresszoneForumApp?.showToast('Network error. Please try again.', 'error');
    }
}
```

---

## Same-Page Hash Navigation

```javascript
// WRONG - Hash change doesn't reload page
window.location.href = '/thread/123/#post-456';
// Page doesn't reload, button stays stuck on "Sending..."

// CORRECT - Force reload after hash navigation
window.location.href = '/thread/123/#post-456';
window.location.reload();

// OR use replace + reload for cleaner history
window.location.replace('/thread/123/#post-456');
window.location.reload();
```

---

## Reduced Motion Support

```javascript
// Check preference
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

// Use in animations
function launchConfetti(particleCount = 50) {
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
    // ... animation code ...
}

// CSS fallback
@media (prefers-reduced-motion: reduce) {
    .presszone-forum-toast {
        animation: none !important;
    }
}
```

---

## Button Loading State Pattern

```javascript
function setButtonLoading(btn, isLoading, loadingText = 'Loading...') {
    if (!btn) return;

    if (isLoading) {
        btn.dataset.originalText = btn.textContent;
        btn.textContent = loadingText;
        btn.disabled = true;
        btn.classList.add('presszone-forum-btn--loading');
    } else {
        btn.textContent = btn.dataset.originalText || btn.textContent;
        btn.disabled = false;
        btn.classList.remove('presszone-forum-btn--loading');
        delete btn.dataset.originalText;
    }
}

// Usage
const submitBtn = form.querySelector('button[type="submit"]');
setButtonLoading(submitBtn, true, 'Submitting...');

try {
    await submitForm(form);
} finally {
    setButtonLoading(submitBtn, false);
}
```

---

## Focus Trap for Modals

```javascript
function createFocusTrap(modal) {
    const focusableSelectors = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
    let previousActiveElement = null;

    function getFocusableElements() {
        return Array.from(modal.querySelectorAll(focusableSelectors)).filter(el => {
            return !el.disabled && el.offsetParent !== null;
        });
    }

    function trapFocus(e) {
        if (e.key !== 'Tab') return;

        const focusable = getFocusableElements();
        if (focusable.length === 0) return;

        const first = focusable[0];
        const last = focusable[focusable.length - 1];

        if (e.shiftKey && document.activeElement === first) {
            e.preventDefault();
            last.focus();
        } else if (!e.shiftKey && document.activeElement === last) {
            e.preventDefault();
            first.focus();
        }
    }

    return {
        activate: function () {
            previousActiveElement = document.activeElement;
            modal.addEventListener('keydown', trapFocus);
            const focusable = getFocusableElements();
            if (focusable.length > 0) {
                setTimeout(() => focusable[0].focus(), 50);
            }
        },
        deactivate: function () {
            modal.removeEventListener('keydown', trapFocus);
            if (previousActiveElement?.focus) {
                previousActiveElement.focus();
            }
        }
    };
}
```

---

## Exclusive UI Opening Pattern

Close other open menus/panels when opening new one:

```javascript
// Dispatch event when opening
document.dispatchEvent(new CustomEvent('fpz:ui-opening', {
    detail: { source: 'notifications' }
}));

// Listen to close when other opens
document.addEventListener('fpz:ui-opening', (e) => {
    if (e.detail.source !== 'user-menu') {
        closeUserMenu();
    }
});

// Full example
function togglePanel(panelName) {
    state.isOpen = !state.isOpen;

    if (state.isOpen) {
        // Notify other UI elements
        document.dispatchEvent(new CustomEvent('fpz:ui-opening', {
            detail: { source: panelName }
        }));
    }

    elements.panel.classList.toggle('presszone-forum-is-open', state.isOpen);
}
```

---

## HTML Escaping

```javascript
function escapeHtml(text) {
    if (!text) return '';
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

// Usage in template literals
const html = `<a href="${escapeHtml(url)}">${escapeHtml(title)}</a>`;
```

---

## localStorage Patterns

```javascript
// Store JSON data
const collapsed = JSON.parse(localStorage.getItem('presszone_forum_collapsed') || '{}');
collapsed[categoryId] = isExpanded;
localStorage.setItem('presszone_forum_collapsed', JSON.stringify(collapsed));

// Listen for storage changes (sync across tabs)
window.addEventListener('storage', (e) => {
    if (e.key === 'presszone-forum-dark-mode') {
        const enabled = e.newValue === 'true';
        document.body.classList.toggle('presszone-forum-dark', enabled);
    }
});
```

---

## Translation (i18n) Rules

### JavaScript Strings - Pass through wp_localize_script

```javascript
// Access localized strings - NEVER hardcode user-facing text
const errorMsg = window.presszoneForumData?.strings?.error || 'Error';
const successMsg = window.presszoneForumData?.strings?.success || 'Success';
```

### Translation Maintenance Rules

| Action | Requirement |
|--------|-------------|
| **Adding new UI string** | Add to `wp_localize_script()` in PHP + add to all `.po` files, translate all languages |
| **Removing UI string** | Remove from `wp_localize_script()` + remove from all `.po` files to avoid bloat |
| **Editing UI string** | Update in PHP source + update all `.po` files, retranslate |

### File Locations
- `languages/forum-press-zone-{locale}.po` - All translatable strings
- Frontend strings passed via `presszoneForumData.strings` in `wp_localize_script()`

---

## Common Mistakes to Avoid

| Mistake | Fix |
|---------|-----|
| Using `FPZ`, `pz`, `fpz` prefix | Use `PresszoneForumApp` (4+ chars required) |
| Bare `FPZ.method()` in 2nd IIFE | Get from window: `const PresszoneForumApp = window.PresszoneForumApp` |
| `presszoneForumData.prop` without check | Use `window.presszoneForumData?.prop \|\| fallback` |
| Missing TinyMCE sync before submit | Call `tinymce.triggerSave()` first |
| Only inline errors, no toast | Always show toast for user-facing errors |
| Hash navigation without reload | Add `window.location.reload()` after href change |
| Missing reduced motion check | Check `prefers-reduced-motion` before animations |
| Direct `document.querySelector` on dynamic elements | Use event delegation |
| Missing `e.preventDefault()` on click handlers | Always prevent default on button/link handlers |
| Forgetting `credentials: 'same-origin'` in fetch | Include for all authenticated requests |

---

## Key Module Reference

### PresszoneForumApp (frontend.js)

| Method | Purpose |
|--------|---------|
| `init()` | Initialize all handlers |
| `showToast(message, type, duration)` | Display toast notification |
| `createConfetti(count)` | Launch confetti celebration |
| `createFocusTrap(modal)` | Create accessible focus trap |
| `initReactions()` | Handle reaction buttons |
| `initVoting()` | Handle voting system |
| `initPolls()` | Handle poll voting |
| `initQuoting()` | Handle quote insertion |
| `initMentions()` | Handle @mentions |
| `initAntiSpam()` | reCAPTCHA integration |

### PresszoneForumEditor (frontend.js)

| Method | Purpose |
|--------|---------|
| `init()` | Initialize all editors on page |
| `initEditor(wrapper)` | Setup single editor instance |
| `executeCommand(editorId, cmd)` | Execute formatting command |
| `insertEmoji(editorId, emoji)` | Insert emoji at cursor |
| `toggleEmojiPicker(wrapper, editorId)` | Show/hide emoji picker |
| `bbcodeToHtml(bbcode)` | Convert BBCode quotes to HTML |
| `htmlToBbcode(html)` | Convert HTML quotes to BBCode |

### PresszoneForumDarkMode (dark-mode.js)

| Method | Purpose |
|--------|---------|
| `isDark()` | Check if dark mode enabled |
| `set(enabled)` | Set dark mode state |
| `toggle()` | Toggle dark mode |

---

## Self-Learning Protocol

Update this file when:

### When to Update

1. **New global object added** - Add to Global Objects table
2. **New IIFE module created** - Add pattern example
3. **Bug pattern identified** - Add to Common Mistakes
4. **New TinyMCE integration** - Add to TinyMCE section
5. **Major refactor** - Update affected sections
6. **New convention established** - Add to Critical Rules

### Update Format

Add to Recent Updates with date + description.

---

## Recent Updates

- **2026-01-04** - Initial creation with full frontend JS knowledge

---

## File Statistics

| File | Lines | Purpose |
|------|-------|---------|
| `frontend.js` | ~3500 | Main frontend (reactions, voting, quotes, mentions, editor) |
| `messenger.js` | ~600 | Instant messaging widget |
| `notifications.js` | ~340 | Notifications panel |
| `presszone-forum-nested-replies.js` | ~800 | Reddit-style replies |
| `dark-mode.js` | ~175 | Dark mode controller |

---

## 🔒 MANDATORY SECURITY & COMPLIANCE RULES

### WordPress.org Compliance (ZERO TOLERANCE)

#### JavaScript Naming - 4+ Characters REQUIRED
```javascript
// CORRECT - WordPress.org compliant (4+ chars)
const PresszoneForumApp = {};
window.presszoneForumData = { ajaxUrl, nonce };
window.PresszoneForumEditor = { /* methods */ };
window.PresszoneForumDarkMode = { isDark, toggle };

// FORBIDDEN - Will cause WordPress.org plugin rejection
const FPZ = {};      // 3 letters - TOO SHORT
const pz = {};       // 2 letters - TOO SHORT
const fpz = {};      // 3 letters - TOO SHORT
FPZ.showToast();     // Using short prefix - WILL FAIL
```

**Why:** WP.org auto-rejects plugins with short global identifiers — prevent namespace collisions.

### Security Rules - ABSOLUTE REQUIREMENTS

#### XSS Prevention - CRITICAL
```javascript
// NEVER use innerHTML with API response data - use DOMParser or textContent
// API responses should be treated as untrusted
element.textContent = response.message;  // CORRECT
element.innerHTML = response.message;    // FORBIDDEN - XSS risk

// ALWAYS escape HTML content
function escapeHtml(text) {
    if (!text) return '';
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

// Use in template literals
const html = `<span>${escapeHtml(user.name)}</span>`;
```

#### CSRF Protection
```javascript
// ALWAYS include nonce in AJAX requests
const options = {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-WP-Nonce': window.presszoneForumData?.nonce || ''  // CRITICAL
    },
    credentials: 'same-origin',  // REQUIRED for authenticated requests
    body: JSON.stringify(data)
};

// ALWAYS validate response status
if (!response.ok) {
    throw new Error(json.message || 'Request failed');
}
```

#### Input Validation
```javascript
// ALWAYS sanitize user input before sending to server
function sanitizeInput(input) {
    if (typeof input !== 'string') return '';
    return input.trim().substring(0, 1000); // Limit length
}

// ALWAYS validate numeric inputs
function validateId(id) {
    const numId = parseInt(id);
    return (numId > 0) ? numId : null;
}

// Use before API calls
const postId = validateId(element.dataset.postId);
if (!postId) {
    console.error('Invalid post ID');
    return;
}
```

#### Safe Data Access
```javascript
// ALWAYS use optional chaining and fallbacks
const ajaxUrl = window.presszoneForumData?.ajaxUrl || '/wp-admin/admin-ajax.php';
const nonce = window.presszoneForumData?.nonce || '';
const errorMsg = window.presszoneForumData?.strings?.error || 'An error occurred';

// FORBIDDEN - May throw if undefined
const ajaxUrl = presszoneForumData.ajaxUrl;  // ReferenceError if not loaded
const nonce = window.presszoneForumData.nonce;  // TypeError if presszoneForumData is undefined
```

### Accessibility Rules - MANDATORY

#### Keyboard Navigation Support
```javascript
// ALWAYS add keyboard handlers (Enter/Space) to custom focusable elements
element.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        handleClick();
    }
});

// ALWAYS ensure proper tab order
element.setAttribute('tabindex', '0');  // Focusable
element.setAttribute('tabindex', '-1'); // Programmatically focusable only
```

#### Focus Management in Modals
```javascript
// ALWAYS manage focus in modals (trap focus, return focus on close)
function createFocusTrap(modal) {
    let previousActiveElement = null;
    
    return {
        activate: function() {
            previousActiveElement = document.activeElement;
            // Focus first focusable element
            const firstFocusable = modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
            if (firstFocusable) {
                setTimeout(() => firstFocusable.focus(), 50);
            }
        },
        deactivate: function() {
            // Return focus to previous element
            if (previousActiveElement?.focus) {
                previousActiveElement.focus();
            }
        }
    };
}
```

#### ARIA Attributes
```javascript
// ALWAYS set aria-expanded on dropdown triggers
function toggleDropdown(trigger, menu) {
    const isOpen = menu.classList.contains('is-open');
    trigger.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
    menu.classList.toggle('is-open');
}

// ALWAYS provide ARIA labels for icon buttons
const button = document.createElement('button');
button.innerHTML = '🗑️';
button.setAttribute('aria-label', 'Delete post');
button.setAttribute('title', 'Delete post');
```

#### Screen Reader Announcements
```javascript
// ALWAYS announce dynamic changes to screen readers
function announceToScreenReader(message) {
    const announcement = document.createElement('div');
    announcement.setAttribute('aria-live', 'polite');
    announcement.setAttribute('aria-atomic', 'true');
    announcement.className = 'presszone-forum-sr-only';
    announcement.textContent = message;
    
    document.body.appendChild(announcement);
    setTimeout(() => announcement.remove(), 1000);
}

// Use after important actions
announceToScreenReader('Post deleted successfully');
```

#### Reduced Motion Support
```javascript
// ALWAYS check preference before animations
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

function launchConfetti(particleCount = 50) {
    if (prefersReducedMotion) return;  // Skip animation
    // ... animation code ...
}

// CSS fallback
@media (prefers-reduced-motion: reduce) {
    .presszone-forum-toast {
        animation: none !important;
    }
}
```

### Data Protection & Privacy

#### Sensitive Data Handling
```javascript
// NEVER log sensitive data
console.log(userData);  // FORBIDDEN if contains PII

// ALWAYS sanitize data for logging
console.log({
    action: 'user_updated',
    user_id: userData.id,  // ID only, no personal info
    timestamp: Date.now()
});

// NEVER store sensitive data in localStorage
localStorage.setItem('user_email', email);  // FORBIDDEN
localStorage.setItem('user_preferences', JSON.stringify(prefs));  // OK
```

#### API Response Validation
```javascript
// ALWAYS validate API response structure
async function fetchData(endpoint) {
    try {
        const response = await fetch(endpoint, {
            headers: { 'X-WP-Nonce': window.presszoneForumData?.nonce || '' }
        });
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        
        const data = await response.json();
        
        // Validate response structure
        if (!data || typeof data !== 'object') {
            throw new Error('Invalid response format');
        }
        
        return data;
    } catch (error) {
        // NEVER expose internal error details to users
        if (window.PresszoneForumApp?.showToast) {
            window.PresszoneForumApp.showToast('Failed to load data', 'error');
        }
        console.error('API Error:', error);  // Log for debugging only
        return null;
    }
}
```

### Performance & Resource Management

#### Memory Management
```javascript
// ALWAYS clean up event listeners and timers
class ComponentManager {
    constructor() {
        this.eventListeners = [];
        this.timers = [];
    }
    
    addListener(element, event, handler) {
        element.addEventListener(event, handler);
        this.eventListeners.push({ element, event, handler });
    }
    
    addTimer(timerId) {
        this.timers.push(timerId);
    }
    
    destroy() {
        // Clean up listeners
        this.eventListeners.forEach(({ element, event, handler }) => {
            element.removeEventListener(event, handler);
        });
        
        // Clear timers
        this.timers.forEach(timerId => clearTimeout(timerId));
        
        this.eventListeners = [];
        this.timers = [];
    }
}
```

#### Rate Limiting & Debouncing
```javascript
// ALWAYS implement debouncing for frequent operations
function debounce(func, wait) {
    let timeout;
    return function executedFunction(...args) {
        const later = () => {
            clearTimeout(timeout);
            func(...args);
        };
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
    };
}

// ALWAYS implement throttling for scroll/resize events
function throttle(func, limit) {
    let inThrottle;
    return function() {
        const args = arguments;
        const context = this;
        if (!inThrottle) {
            func.apply(context, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
        }
    };
}

// Use for search, auto-save, etc.
const debouncedSearch = debounce(performSearch, 300);
const throttledScroll = throttle(handleScroll, 100);
```

### Error Handling & User Experience

#### User-Friendly Error Messages
```javascript
// ALWAYS provide helpful error messages
function handleApiError(error, context = '') {
    let userMessage;
    
    if (error.status) {
        switch (error.status) {
            case 403:
                userMessage = 'You do not have permission to perform this action';
                break;
            case 404:
                userMessage = 'The requested item was not found';
                break;
            case 429:
                userMessage = 'Too many requests. Please wait and try again';
                break;
            case 500:
                userMessage = 'Server error. Please try again later';
                break;
            default:
                userMessage = 'An unexpected error occurred. Please try again';
        }
    } else {
        userMessage = 'Network error. Please check your connection';
    }
    
    if (window.PresszoneForumApp?.showToast) {
        window.PresszoneForumApp.showToast(userMessage, 'error');
    }
    
    // Log detailed error for debugging
    console.error(`${context} Error:`, error);
}
```

#### Graceful Degradation
```javascript
// ALWAYS provide fallbacks for failed operations
async function saveData(data) {
    try {
        await apiRequest('/save', 'POST', data);
        if (window.PresszoneForumApp?.showToast) {
            window.PresszoneForumApp.showToast('Data saved successfully', 'success');
        }
    } catch (error) {
        // Fallback: Store locally and retry later
        localStorage.setItem('presszone_forum_pending_data', JSON.stringify({
            data,
            timestamp: Date.now()
        }));
        
        if (window.PresszoneForumApp?.showToast) {
            window.PresszoneForumApp.showToast('Data saved locally. Will sync when connection is restored', 'warning');
        }
        
        // Schedule retry
        setTimeout(() => retryPendingOperations(), 30000);
    }
}
```

### TinyMCE Integration Security

#### Content Sanitization
```javascript
// ALWAYS sync TinyMCE before form submit - CRITICAL
form.addEventListener('submit', async (e) => {
    e.preventDefault();

    // CRITICAL - Sync TinyMCE to textarea first
    if (typeof tinymce !== 'undefined') {
        tinymce.triggerSave();
        // Or for a specific editor:
        const editor = tinymce.get('message');
        if (editor) {
            editor.save();
        }
    }

    const formData = new FormData(form);
    // ... submit ...
});
```

#### Safe Content Insertion
```javascript
// ALWAYS sanitize content before inserting into editor
function insertContent(editor, content) {
    // Sanitize content
    const sanitized = content.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
    
    if (editor && !editor.isHidden()) {
        editor.insertContent(sanitized);
        editor.focus();
    } else {
        // Fallback to textarea
        const textarea = document.getElementById(editor.id);
        if (textarea) {
            const start = textarea.selectionStart;
            textarea.value = textarea.value.substring(0, start) + sanitized + textarea.value.substring(start);
            textarea.focus();
            textarea.setSelectionRange(start + sanitized.length, start + sanitized.length);
        }
    }
}
```

### Testing Requirements

#### Unit Testing
```javascript
// ALWAYS test critical functions
describe('API Request Security', () => {
    it('includes nonce header in requests', async () => {
        const mockFetch = jest.fn().mockResolvedValue({
            ok: true,
            json: () => Promise.resolve({ success: true })
        });
        global.fetch = mockFetch;
        
        await apiRequest('/test', 'POST', { data: 'test' });
        
        expect(mockFetch).toHaveBeenCalledWith(
            expect.any(String),
            expect.objectContaining({
                headers: expect.objectContaining({
                    'X-WP-Nonce': expect.any(String)
                })
            })
        );
    });
    
    it('escapes HTML in user content', () => {
        const maliciousInput = '<script>alert("xss")</script>';
        const escaped = escapeHtml(maliciousInput);
        expect(escaped).not.toContain('<script>');
        expect(escaped).toContain('&lt;script&gt;');
    });
});
```

#### Integration Testing
```javascript
// ALWAYS test user workflows
describe('Post Creation Workflow', () => {
    it('creates post with proper validation', async () => {
        // Mock TinyMCE
        global.tinymce = {
            triggerSave: jest.fn(),
            get: jest.fn().mockReturnValue({
                save: jest.fn()
            })
        };
        
        const form = document.createElement('form');
        const textarea = document.createElement('textarea');
        textarea.value = 'Test post content';
        form.appendChild(textarea);
        
        // Simulate form submission
        const submitEvent = new Event('submit');
        form.dispatchEvent(submitEvent);
        
        expect(tinymce.triggerSave).toHaveBeenCalled();
    });
});
```

---

## Quick Debugging

```javascript
// Check if PresszoneForumApp is available
console.log('PresszoneForumApp:', window.PresszoneForumApp);

// Check localized data
console.log('presszoneForumData:', window.presszoneForumData);

// Check TinyMCE editors
if (typeof tinymce !== 'undefined') {
    console.log('TinyMCE editors:', tinymce.editors.map(e => e.id));
}

// Check dark mode state
console.log('Dark mode:', window.PresszoneForumDarkMode?.isDark());
```