# Frontend Architecture Skill

> **Technology:** Component-based vanilla JavaScript architecture for admin and frontend

---

## Purpose

This skill covers component patterns, state management, module organization, and build architecture for the Comments Press Zone plugin.

---

## Architecture Overview

### Admin SPA Structure

```
admin/src-vanilla/
├── admin.js                 # Entry point, router
├── components/              # 23 reusable UI components
│   ├── Button.js
│   ├── Card.js
│   ├── Modal.js
│   ├── Toast.js
│   └── ...
├── pages/                   # Page modules
│   ├── dashboard.js
│   ├── moderation.js
│   ├── settings.js
│   └── ...
└── utils/
    ├── api.js              # REST client
    ├── dom.js              # DOM helpers
    └── confirm.js          # Confirmation dialogs
```

### Frontend Structure

```
assets/js/
├── frontend.js             # Entry point (IIFE)
├── components/             # ES Modules
│   ├── Editor.js
│   ├── Modal.js
│   ├── Confetti.js
│   └── EmojiPicker.js
└── dark-mode.js            # Dark mode controller
```

---

## Component Pattern

### Factory Function (Simple Components)

```javascript
// components/Button.js
export function Button(options) {
    const {
        label,
        variant = 'primary',
        size = 'md',
        icon = '',
        onClick,
        disabled = false,
        type = 'button',
    } = options;
    
    const button = document.createElement('button');
    button.type = type;
    button.className = `presszone-comments-btn presszone-comments-btn--${variant} presszone-comments-btn--${size}`;
    button.disabled = disabled;
    
    if (icon) {
        const iconEl = document.createElement('span');
        iconEl.className = 'presszone-comments-btn__icon';
        iconEl.textContent = icon;
        button.appendChild(iconEl);
    }
    
    const labelEl = document.createElement('span');
    labelEl.textContent = label;
    button.appendChild(labelEl);
    
    if (onClick) {
        button.onclick = onClick;
    }
    
    return button;
}

// Helper variants
export function SaveButton(label, onClick, disabled = false) {
    return Button({ label, variant: 'primary', icon: '💾', onClick, disabled });
}

export function CancelButton(onClick) {
    return Button({ label: 'Cancel', variant: 'secondary', onClick });
}
```

### Class-Based Component (Complex Components)

```javascript
// components/Modal.js
export default class Modal {
    constructor(options = {}) {
        this.options = {
            title: '',
            width: '600px',
            premium: false,
            icon: '',
            onClose: null,
            ...options
        };
        
        this.element = null;
        this.isOpen = false;
        
        this.create();
    }
    
    create() {
        this.element = document.createElement('div');
        this.element.className = 'presszone-comments-modal';
        this.element.style.display = 'none';
        
        const overlay = document.createElement('div');
        overlay.className = 'presszone-comments-modal__overlay';
        overlay.onclick = () => this.close();
        
        const content = document.createElement('div');
        content.className = 'presszone-comments-modal__content';
        content.style.maxWidth = this.options.width;
        
        this.element.appendChild(overlay);
        this.element.appendChild(content);
        document.body.appendChild(this.element);
    }
    
    render(bodyContent, actions = [], tabs = null) {
        const content = this.element.querySelector('.presszone-comments-modal__content');
        content.innerHTML = '';
        
        // Header
        const header = this.createHeader();
        content.appendChild(header);
        
        // Tabs (if provided)
        if (tabs) {
            const tabsEl = this.createTabs(tabs);
            content.appendChild(tabsEl);
        }
        
        // Body
        const body = document.createElement('div');
        body.className = 'presszone-comments-modal__body';
        body.appendChild(bodyContent);
        content.appendChild(body);
        
        // Footer
        if (actions.length > 0) {
            const footer = this.createFooter(actions);
            content.appendChild(footer);
        }
    }
    
    open() {
        this.isOpen = true;
        this.element.style.display = 'flex';
        this.previousFocus = document.activeElement;
        this.element.querySelector('.presszone-comments-modal__close')?.focus();
        this.trapFocus();
    }
    
    close() {
        this.isOpen = false;
        this.element.style.display = 'none';
        if (this.previousFocus) {
            this.previousFocus.focus();
        }
        if (this.options.onClose) {
            this.options.onClose();
        }
    }
    
    trapFocus() {
        const focusable = this.element.querySelectorAll(
            'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
        );
        
        if (focusable.length === 0) return;
        
        const first = focusable[0];
        const last = focusable[focusable.length - 1];
        
        this.element.addEventListener('keydown', (e) => {
            if (e.key === 'Escape') {
                this.close();
            } else if (e.key === 'Tab') {
                if (e.shiftKey && document.activeElement === first) {
                    e.preventDefault();
                    last.focus();
                } else if (!e.shiftKey && document.activeElement === last) {
                    e.preventDefault();
                    first.focus();
                }
            }
        });
    }
}
```

---

## State Management Pattern (Module-Level)

```javascript
// pages/settings.js
let settings = {};
let originalSettings = {};
let isDirty = false;
let activeTab = 'general';

async function loadSettings() {
    const response = await API.get('/settings');
    settings = response.data;
    originalSettings = { ...settings };
    isDirty = false;
}

function updateSetting(key, value) {
    settings[key] = value;
    isDirty = hasChanges();
}

function hasChanges() {
    return JSON.stringify(settings) !== JSON.stringify(originalSettings);
}

async function saveSettings() {
    try {
        await API.post('/settings', settings);
        originalSettings = { ...settings };
        isDirty = false;
        Toast.success('Settings saved!');
    } catch (error) {
        Toast.error(error.message);
    }
}
```

---

## Router Pattern (Admin SPA)

```javascript
// admin.js
class AdminRouter {
    constructor() {
        this.routes = {
            '': this.renderDashboard,
            '#/': this.renderDashboard,
            '#/moderation': this.renderModeration,
            '#/bans': this.renderBans,
            '#/settings': this.renderSettings,
            '#/settings/emails': this.renderSettings, // Sub-route
            '#/design': this.renderDesign,
            '#/tools': this.renderTools,
        };
        
        this.container = document.querySelector('#presszone-comments-content');
        this.init();
    }
    
    init() {
        window.addEventListener('hashchange', () => this.route());
        this.route();
    }
    
    route() {
        const hash = window.location.hash;
        const handler = this.routes[hash] || this.routes[''];
        
        // Extract sub-route (e.g., #/settings/emails -> 'emails')
        const parts = hash.split('/');
        const subRoute = parts.length > 2 ? parts[2] : null;
        
        handler.call(this, this.container, subRoute);
    }
    
    renderDashboard(container) {
        // Load dashboard page
        import('./pages/dashboard.js').then(module => {
            module.render(container);
        });
    }
}

// Initialize router
document.addEventListener('DOMContentLoaded', () => {
    new AdminRouter();
});
```

---

## API Client Pattern

```javascript
// utils/api.js
class APIClient {
    constructor() {
        this.config = window.presszoneCommentsAdmin || {};
        this.baseUrl = this.config.apiUrl || '';
        this.nonce = this.config.nonce || '';
    }
    
    async get(endpoint) {
        return this.request(endpoint, 'GET');
    }
    
    async post(endpoint, data) {
        return this.request(endpoint, 'POST', data);
    }
    
    async put(endpoint, data) {
        return this.request(endpoint, 'PUT', data);
    }
    
    async delete(endpoint) {
        return this.request(endpoint, 'DELETE');
    }
    
    async request(endpoint, method, data = null) {
        const url = this.baseUrl + endpoint;
        const options = {
            method,
            headers: {
                'X-WP-Nonce': this.nonce,
                'Content-Type': 'application/json',
            },
            credentials: 'same-origin',
        };
        
        if (data && (method === 'POST' || method === 'PUT')) {
            options.body = JSON.stringify(data);
        }
        
        try {
            const response = await fetch(url, options);
            const result = await response.json();
            
            if (!response.ok) {
                throw new Error(result.message || 'Request failed');
            }
            
            return result;
        } catch (error) {
            console.error('API Error:', error);
            throw error;
        }
    }
}

// Export singleton
export default new APIClient();
```

---

## Dynamic Imports (Code Splitting)

```javascript
// Lazy load components
async function openEditor() {
    const { default: Editor } = await import('./components/Editor.js');
    const editor = new Editor({
        onSubmit: (data) => handleSubmit(data)
    });
    editor.open();
}

// Lazy load pages
async function renderSettings(container) {
    const module = await import('./pages/settings.js');
    module.render(container);
}
```

---

## DOM Utilities

```javascript
// utils/dom.js

// Create element helper
export function el(tag, attrs = {}, ...children) {
    const element = document.createElement(tag);
    
    Object.entries(attrs).forEach(([key, value]) => {
        if (key === 'className') {
            element.className = value;
        } else if (key.startsWith('on')) {
            element[key] = value;
        } else {
            element.setAttribute(key, value);
        }
    });
    
    children.forEach(child => {
        if (typeof child === 'string') {
            element.appendChild(document.createTextNode(child));
        } else if (child) {
            element.appendChild(child);
        }
    });
    
    return element;
}

// Query helpers
export const qs = (selector) => document.querySelector(selector);
export const qsa = (selector) => document.querySelectorAll(selector);

// Clear container
export function clear(element) {
    while (element.firstChild) {
        element.removeChild(element.firstChild);
    }
}

// Mount child to parent
export function mount(parent, child) {
    parent.appendChild(child);
}
```

---

## Build Configuration

```javascript
// admin/webpack.config.js
const path = require('path');

module.exports = {
    entry: './src-vanilla/admin.js',
    output: {
        filename: 'admin.js',
        path: path.resolve(__dirname, 'build'),
    },
    module: {
        rules: [
            {
                test: /\.scss$/,
                use: ['style-loader', 'css-loader', 'sass-loader'],
            },
        ],
    },
    mode: 'production',
};
```

---

## Common Mistakes to Avoid

| Mistake | Fix |
|---------|-----|
| Global state pollution | Use module-level state or class instances |
| No component isolation | Each component should be self-contained |
| Inline styles in JS | Use CSS classes with SCSS variables |
| Not cleaning up event listeners | Remove listeners when destroying components |
| Synchronous imports for large modules | Use dynamic `import()` for code splitting |
| Direct DOM manipulation without helpers | Use utility functions for consistency |

---

## Testing Checklist

- [ ] Components are reusable and isolated
- [ ] State management clear and predictable
- [ ] Router handles all defined routes
- [ ] API client handles errors gracefully
- [ ] Dynamic imports work for code splitting
- [ ] Event listeners cleaned up on destroy
- [ ] DOM helpers used consistently
- [ ] Build process produces optimized output
