# Skill: Admin Panel Full-Stack

## Identity
- **Skill ID**: `admin-panel-fullstack`
- **Domain**: WordPress Admin Panel Development
- **Technologies**: Vanilla JS + Webpack + SCSS + PHP REST API
- **Source Agent**: `admin-panel-expert.md`

## When to Load This Skill
- Task involves admin dashboard development
- Building admin UI components
- Creating admin REST endpoints
- Working with hash-based SPA routing
- Files matching: `admin/src-vanilla/**`, `includes/Api/Rest*.php`

## Core Patterns

### JavaScript Architecture (NO Frameworks)
```javascript
// Entry point pattern (admin.js)
const routes = {
    '': renderDashboard,
    '#/': renderDashboard,
    '#/languages': renderLanguages,
    '#/translations': renderTranslations,
    '#/settings': renderSettings,
};

function router() {
    const hash = location.hash || '';
    const [route, ...params] = hash.split('/').filter(Boolean);
    const routeKey = params.length ? `#/${route}` : hash || '';

    const handler = routes[routeKey] || render404;
    handler(container, params);
}

window.addEventListener('hashchange', router);
document.addEventListener('DOMContentLoaded', router);
```

### DOM Utilities (el, qs, qsa)
```javascript
// Create elements (JSX-like without JSX)
function el(tag, attrs = {}, ...children) {
    const element = document.createElement(tag);

    Object.entries(attrs).forEach(([key, value]) => {
        if (key === 'class') element.className = value;
        else if (key.startsWith('on')) element[key] = value;
        else if (value !== null && value !== undefined) {
            element.setAttribute(key, value);
        }
    });

    children.flat().forEach(child => {
        if (typeof child === 'string') {
            element.appendChild(document.createTextNode(child));
        } else if (child) {
            element.appendChild(child);
        }
    });

    return element;
}

const qs = (sel, ctx = document) => ctx.querySelector(sel);
const qsa = (sel, ctx = document) => [...ctx.querySelectorAll(sel)];
const __ = (text) => wp.i18n.__(text, 'international-press-zone');
```

### Component Patterns

#### Button Component
```javascript
function Button({ label, variant = 'secondary', onClick, disabled = false, icon = '' }) {
    return el('button', {
        class: `presszone-international-btn presszone-international-btn--${variant}`,
        onclick: onClick,
        disabled: disabled,
        type: 'button'
    }, icon ? el('span', { 'aria-hidden': 'true' }, icon) : null, ' ', label);
}

// Helpers
const SaveButton = (label, onClick, disabled) =>
    Button({ label: label || __('Save'), variant: 'primary', onClick, disabled });

const DeleteButton = (onClick, label) =>
    Button({ label: label || __('Delete'), variant: 'danger', onClick });
```

#### Card Component
```javascript
function Card(icon, title, description, children = [], options = {}) {
    const { collapsible = false, collapsed = false, staggerIndex = 0 } = options;

    return el('div', {
        class: `presszone-international-card presszone-international-stagger-${Math.min(staggerIndex, 5)}`,
        'data-collapsed': collapsed ? 'true' : null
    },
        el('div', { class: 'presszone-international-card__header' },
            icon ? el('span', { class: 'presszone-international-card__icon', 'aria-hidden': 'true' }, icon) : null,
            el('div', { class: 'presszone-international-card__title' },
                el('h3', {}, title),
                description ? el('p', {}, description) : null
            ),
            collapsible ? el('button', {
                class: 'presszone-international-card__toggle',
                'aria-expanded': !collapsed,
                'aria-label': __('Toggle section')
            }, collapsed ? '+' : '-') : null
        ),
        el('div', { class: 'presszone-international-card__body' }, ...children)
    );
}
```

#### Toast (MANDATORY for notifications)
```javascript
const Toast = {
    container: null,

    init() {
        if (!this.container) {
            this.container = el('div', {
                class: 'presszone-international-toast-container',
                'aria-live': 'polite',
                'aria-atomic': 'true'
            });
            document.body.appendChild(this.container);
        }
    },

    show({ message, type = 'info', duration = 5000 }) {
        this.init();

        const toast = el('div', {
            class: `presszone-international-toast presszone-international-toast--${type}`,
            role: 'alert'
        }, message);

        this.container.appendChild(toast);

        setTimeout(() => toast.remove(), duration);
    },

    success: (msg) => Toast.show({ message: msg, type: 'success' }),
    error: (msg) => Toast.show({ message: msg, type: 'error' }),
    warning: (msg) => Toast.show({ message: msg, type: 'warning' }),
    info: (msg) => Toast.show({ message: msg, type: 'info' })
};
```

### State Management Pattern
```javascript
// Module-level state (no Redux)
let settings = {};
let originalSettings = {};
let isDirty = false;

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

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

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

### API Client Pattern
```javascript
const API = {
    baseUrl: window.presszoneInternationalAdmin?.apiUrl || '/wp-json/international-press-zone/v1',
    nonce: window.presszoneInternationalAdmin?.nonce || '',

    async request(endpoint, options = {}) {
        const response = await fetch(`${this.baseUrl}${endpoint}`, {
            ...options,
            headers: {
                'Content-Type': 'application/json',
                'X-WP-Nonce': this.nonce,
                ...options.headers
            },
            credentials: 'same-origin'
        });

        if (!response.ok) {
            const error = await response.json().catch(() => ({}));
            throw new Error(error.message || 'Request failed');
        }

        return response.json();
    },

    get: (endpoint) => API.request(endpoint),
    post: (endpoint, data) => API.request(endpoint, { method: 'POST', body: JSON.stringify(data) }),
    put: (endpoint, data) => API.request(endpoint, { method: 'PUT', body: JSON.stringify(data) }),
    delete: (endpoint) => API.request(endpoint, { method: 'DELETE' })
};
```

### REST Endpoint (PHP)
```php
register_rest_route('international-press-zone/v1', '/languages', [
    'methods' => 'GET',
    'callback' => [$this, 'get_languages'],
    'permission_callback' => function() {
        return current_user_can('manage_options');
    }
]);

register_rest_route('international-press-zone/v1', '/languages/(?P<id>\d+)', [
    'methods' => 'PUT',
    'callback' => [$this, 'update_language'],
    'permission_callback' => function() {
        return current_user_can('manage_options');
    },
    'args' => [
        'id' => [
            'required' => true,
            'validate_callback' => function($param) {
                return is_numeric($param) && $param > 0;
            },
            'sanitize_callback' => 'absint'
        ]
    ]
]);
```

## Anti-Patterns (Forbidden)

| Mistake | Fix |
|---------|-----|
| Using React/Vue/Angular | Vanilla JS only |
| Creating inline notifications | Use `Toast` component |
| Using `<table>` elements | Use `<div>` with CSS Grid/Flexbox |
| CSS custom properties for logic | Use SCSS variables with classes |
| Inline styles for dynamic values | Use explicit CSS classes |
| Class chaining `.a.b` | Use single BEM class |
| Magic numbers | Use SCSS variables |
| Missing translation wrapper | Use `__('text')` |
| `api.del()` | Use `API.delete()` |
| Forgetting to rebuild | Run `npm run build` in `admin/` |

## WordPress.org Compliance

### Script Enqueuing
```php
add_action('admin_enqueue_scripts', function($hook) {
    if ($hook !== 'toplevel_page_international-press-zone') {
        return;
    }

    wp_enqueue_script(
        'presszone-international-admin',
        plugins_url('admin/build/admin.js', __FILE__),
        ['wp-i18n'],
        PRESSZONE_INTERNATIONAL_VERSION,
        true
    );

    wp_set_script_translations('presszone-international-admin', 'international-press-zone');

    wp_localize_script('presszone-international-admin', 'presszoneInternationalAdmin', [
        'apiUrl' => rest_url('international-press-zone/v1'),
        'nonce' => wp_create_nonce('wp_rest'),
    ]);
});
```

### Build Commands
| Change Type | Command | Directory |
|-------------|---------|-----------|
| Admin JS/SCSS | `npm run build` | `admin/` |

## Integration with Other Skills
- **Often combined with**: `wordpress-php-integration`, `frontend-styling-scss`
- **For database operations**: Load `database-operations`
- **For settings pages**: Load `settings-management`

## Quick Reference

### Directory Structure
```
admin/
├── src-vanilla/
│   ├── admin.js           # Entry point
│   ├── css/               # SCSS source
│   ├── components/        # UI components
│   ├── pages/             # Page modules
│   └── utils/             # Helpers (api.js, dom.js)
├── build/
│   ├── admin.js           # Compiled JS
│   └── admin.css          # Compiled CSS
└── webpack.config.js
```

### Loading States
```javascript
function showLoading(container, message = __('Loading...')) {
    // Clear container safely
    while (container.firstChild) {
        container.removeChild(container.firstChild);
    }
    container.appendChild(
        el('div', { class: 'presszone-international-loading' },
            el('div', { class: 'presszone-international-spinner', 'aria-hidden': 'true' }),
            el('span', {}, message)
        )
    );
}
```

## Validation Checklist
- [ ] Vanilla JS only (no frameworks)
- [ ] All notifications use Toast component
- [ ] No `<table>` elements (use Grid/Flexbox)
- [ ] SCSS variables, no CSS custom properties
- [ ] API calls include nonce
- [ ] REST endpoints have permission_callback
- [ ] Build ran after changes
- [ ] All strings use translation wrapper
- [ ] 4+ character prefix on all identifiers
