mirror of
https://github.com/thecyberlearn/quantum-ai.git
synced 2026-08-18 17:52:59 +00:00
910 lines
22 KiB
Markdown
910 lines
22 KiB
Markdown
# Template Architecture Patterns
|
|
|
|
Technical reference for implementing optimized Django templates with modern frontend patterns.
|
|
|
|
## Overview
|
|
|
|
This document provides detailed technical specifications for the template architecture used in optimized agents. The patterns ensure consistent UI/UX, maintainable code, and optimal performance.
|
|
|
|
## Core Architecture Principles
|
|
|
|
### 1. Widget-Based Component System
|
|
- **Modular Design**: Each UI component is self-contained
|
|
- **Reusable Patterns**: Components can be easily replicated across agents
|
|
- **Consistent Styling**: All components follow the same design system
|
|
- **Responsive Layout**: Components adapt to different screen sizes
|
|
|
|
### 2. Self-Contained Styles
|
|
- **No External Dependencies**: All styles are embedded in the template
|
|
- **CSS Custom Properties**: Centralized design tokens for consistency
|
|
- **Optimized Performance**: Reduced HTTP requests and faster loading
|
|
- **Maintainable Code**: Easy to update and modify styles
|
|
|
|
### 3. Security-First JavaScript
|
|
- **HTML Sanitization**: All dynamic content is sanitized
|
|
- **XSS Prevention**: Input validation and output encoding
|
|
- **Safe DOM Manipulation**: Controlled content insertion
|
|
- **Event Management**: Proper listener cleanup and memory management
|
|
|
|
## Template Structure
|
|
|
|
### Base Template Integration
|
|
```html
|
|
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}Agent Name - NetCop AI Hub{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
/* Agent-specific optimized styles */
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- Agent content -->
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
/* Agent-specific JavaScript */
|
|
</script>
|
|
{% endblock %}
|
|
```
|
|
|
|
### Layout Grid System
|
|
```html
|
|
<div class="agent-container">
|
|
<div class="agent-grid">
|
|
<!-- Main Content Area -->
|
|
<div class="agent-main">
|
|
<div class="agent-header">
|
|
<h1>{{ agent.name }}</h1>
|
|
<p>{{ agent.description }}</p>
|
|
</div>
|
|
|
|
<div class="agent-form">
|
|
<!-- Form components -->
|
|
</div>
|
|
|
|
<div class="agent-output">
|
|
<!-- Output display -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sidebar Widgets -->
|
|
<div class="agent-sidebar">
|
|
<!-- Widget components -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
## CSS Architecture
|
|
|
|
### Design System Variables
|
|
```css
|
|
:root {
|
|
/* Color Palette */
|
|
--primary: #000000;
|
|
--surface: #ffffff;
|
|
--surface-variant: #f8fafc;
|
|
--background: #f3f4f6;
|
|
--outline: #e4e7eb;
|
|
--outline-variant: #e1e4e7;
|
|
--on-surface: #1a1a1a;
|
|
--on-surface-variant: #6b7280;
|
|
--success: #10b981;
|
|
--error: #ef4444;
|
|
--warning: #f59e0b;
|
|
--info: #3b82f6;
|
|
|
|
/* Border Radius */
|
|
--radius-xs: 4px;
|
|
--radius-sm: 8px;
|
|
--radius-md: 12px;
|
|
--radius-lg: 16px;
|
|
--radius-xl: 20px;
|
|
--radius-2xl: 24px;
|
|
--radius-full: 9999px;
|
|
|
|
/* Spacing Scale */
|
|
--spacing-xs: 4px;
|
|
--spacing-sm: 8px;
|
|
--spacing-md: 16px;
|
|
--spacing-lg: 24px;
|
|
--spacing-xl: 32px;
|
|
--spacing-2xl: 48px;
|
|
--spacing-3xl: 64px;
|
|
|
|
/* Typography */
|
|
--font-size-xs: 0.75rem;
|
|
--font-size-sm: 0.875rem;
|
|
--font-size-base: 1rem;
|
|
--font-size-lg: 1.125rem;
|
|
--font-size-xl: 1.25rem;
|
|
--font-size-2xl: 1.5rem;
|
|
--font-size-3xl: 1.875rem;
|
|
|
|
/* Font Weights */
|
|
--font-weight-normal: 400;
|
|
--font-weight-medium: 500;
|
|
--font-weight-semibold: 600;
|
|
--font-weight-bold: 700;
|
|
|
|
/* Line Heights */
|
|
--line-height-tight: 1.25;
|
|
--line-height-normal: 1.5;
|
|
--line-height-relaxed: 1.75;
|
|
|
|
/* Shadows */
|
|
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
--shadow-lg: 0 10px 20px rgba(0, 0, 0, 0.15);
|
|
--shadow-xl: 0 20px 40px rgba(0, 0, 0, 0.2);
|
|
|
|
/* Transitions */
|
|
--transition-fast: 0.15s ease;
|
|
--transition-base: 0.2s ease;
|
|
--transition-slow: 0.3s ease;
|
|
|
|
/* Z-Index Scale */
|
|
--z-dropdown: 1000;
|
|
--z-sticky: 1020;
|
|
--z-fixed: 1030;
|
|
--z-modal-backdrop: 1040;
|
|
--z-modal: 1050;
|
|
--z-popover: 1060;
|
|
--z-tooltip: 1070;
|
|
}
|
|
```
|
|
|
|
### Component Architecture
|
|
```css
|
|
/* Base Component Styles */
|
|
.component {
|
|
/* Use design system variables */
|
|
background: var(--surface);
|
|
border: 1px solid var(--outline);
|
|
border-radius: var(--radius-md);
|
|
padding: var(--spacing-md);
|
|
transition: var(--transition-base);
|
|
}
|
|
|
|
/* Widget Base Class */
|
|
.widget {
|
|
@extend .component;
|
|
margin-bottom: var(--spacing-md);
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.widget:hover {
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
.widget-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.widget-title {
|
|
font-size: var(--font-size-lg);
|
|
font-weight: var(--font-weight-semibold);
|
|
color: var(--on-surface);
|
|
margin: 0;
|
|
}
|
|
|
|
.widget-content {
|
|
color: var(--on-surface-variant);
|
|
line-height: var(--line-height-normal);
|
|
}
|
|
```
|
|
|
|
### Grid System
|
|
```css
|
|
/* Responsive Grid Layout */
|
|
.agent-container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: var(--spacing-lg);
|
|
}
|
|
|
|
.agent-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 300px;
|
|
gap: var(--spacing-xl);
|
|
align-items: start;
|
|
}
|
|
|
|
/* Responsive Breakpoints */
|
|
@media (max-width: 768px) {
|
|
.agent-grid {
|
|
grid-template-columns: 1fr;
|
|
gap: var(--spacing-lg);
|
|
}
|
|
|
|
.agent-sidebar {
|
|
order: -1; /* Move sidebar to top on mobile */
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.agent-container {
|
|
padding: var(--spacing-md);
|
|
}
|
|
|
|
.agent-grid {
|
|
gap: var(--spacing-md);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Form Component Patterns
|
|
```css
|
|
/* Form Base Styles */
|
|
.form-group {
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.form-label {
|
|
display: block;
|
|
font-weight: var(--font-weight-medium);
|
|
color: var(--on-surface);
|
|
margin-bottom: var(--spacing-sm);
|
|
}
|
|
|
|
.form-control {
|
|
width: 100%;
|
|
padding: var(--spacing-md);
|
|
border: 1px solid var(--outline);
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-base);
|
|
background: var(--surface);
|
|
color: var(--on-surface);
|
|
transition: var(--transition-base);
|
|
}
|
|
|
|
.form-control:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.form-control.is-invalid {
|
|
border-color: var(--error);
|
|
}
|
|
|
|
.form-control.is-valid {
|
|
border-color: var(--success);
|
|
}
|
|
|
|
/* Form Validation Styles */
|
|
.form-error {
|
|
color: var(--error);
|
|
font-size: var(--font-size-sm);
|
|
margin-top: var(--spacing-xs);
|
|
}
|
|
|
|
.form-help {
|
|
color: var(--on-surface-variant);
|
|
font-size: var(--font-size-sm);
|
|
margin-top: var(--spacing-xs);
|
|
}
|
|
|
|
/* Required Field Indicator */
|
|
.required::after {
|
|
content: " *";
|
|
color: var(--error);
|
|
}
|
|
```
|
|
|
|
### Button Component System
|
|
```css
|
|
/* Button Base */
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border: 1px solid transparent;
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-base);
|
|
font-weight: var(--font-weight-medium);
|
|
line-height: var(--line-height-tight);
|
|
text-decoration: none;
|
|
cursor: pointer;
|
|
transition: var(--transition-base);
|
|
user-select: none;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.btn:focus {
|
|
outline: none;
|
|
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* Button Variants */
|
|
.btn-primary {
|
|
background: var(--primary);
|
|
color: var(--surface);
|
|
}
|
|
|
|
.btn-primary:hover:not(:disabled) {
|
|
background: color-mix(in srgb, var(--primary) 90%, black);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: var(--surface-variant);
|
|
color: var(--on-surface);
|
|
}
|
|
|
|
.btn-secondary:hover:not(:disabled) {
|
|
background: color-mix(in srgb, var(--surface-variant) 90%, black);
|
|
}
|
|
|
|
.btn-outline {
|
|
background: transparent;
|
|
color: var(--primary);
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.btn-outline:hover:not(:disabled) {
|
|
background: var(--primary);
|
|
color: var(--surface);
|
|
}
|
|
|
|
/* Button Sizes */
|
|
.btn-sm {
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.btn-lg {
|
|
padding: var(--spacing-md) var(--spacing-lg);
|
|
font-size: var(--font-size-lg);
|
|
}
|
|
|
|
/* Button States */
|
|
.btn-loading {
|
|
position: relative;
|
|
color: transparent;
|
|
}
|
|
|
|
.btn-loading::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
width: 16px;
|
|
height: 16px;
|
|
margin: -8px 0 0 -8px;
|
|
border: 2px solid transparent;
|
|
border-top-color: currentColor;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
```
|
|
|
|
## JavaScript Architecture
|
|
|
|
### Module Pattern
|
|
```javascript
|
|
// Agent Module Pattern
|
|
const AgentModule = (function() {
|
|
'use strict';
|
|
|
|
// Private variables
|
|
let isInitialized = false;
|
|
let eventListeners = [];
|
|
let config = {};
|
|
|
|
// Private methods
|
|
function init() {
|
|
if (isInitialized) return;
|
|
|
|
setupEventListeners();
|
|
setupFormValidation();
|
|
setupWalletIntegration();
|
|
setupAccessibility();
|
|
|
|
isInitialized = true;
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
// Event listener setup with cleanup tracking
|
|
}
|
|
|
|
function setupFormValidation() {
|
|
// Form validation setup
|
|
}
|
|
|
|
function setupWalletIntegration() {
|
|
// Wallet integration setup
|
|
}
|
|
|
|
function setupAccessibility() {
|
|
// Accessibility enhancements
|
|
}
|
|
|
|
// Public API
|
|
return {
|
|
init: init,
|
|
destroy: function() {
|
|
// Cleanup method
|
|
eventListeners.forEach(({element, event, handler}) => {
|
|
if (element) {
|
|
element.removeEventListener(event, handler);
|
|
}
|
|
});
|
|
eventListeners = [];
|
|
isInitialized = false;
|
|
},
|
|
|
|
// Public methods
|
|
submitForm: function(formData) {
|
|
// Form submission logic
|
|
},
|
|
|
|
updateWalletBalance: function(newBalance) {
|
|
// Wallet balance update logic
|
|
},
|
|
|
|
showError: function(message) {
|
|
// Error display logic
|
|
},
|
|
|
|
showSuccess: function(message) {
|
|
// Success display logic
|
|
}
|
|
};
|
|
})();
|
|
|
|
// Initialize when DOM is ready
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
AgentModule.init();
|
|
});
|
|
```
|
|
|
|
### State Management Pattern
|
|
```javascript
|
|
// Simple State Management
|
|
const AgentState = {
|
|
// Initial state
|
|
data: {
|
|
isLoading: false,
|
|
walletBalance: 0,
|
|
formData: {},
|
|
results: null,
|
|
errors: []
|
|
},
|
|
|
|
// State update method
|
|
setState(newState) {
|
|
this.data = { ...this.data, ...newState };
|
|
this.render();
|
|
},
|
|
|
|
// Get current state
|
|
getState() {
|
|
return { ...this.data };
|
|
},
|
|
|
|
// Render method
|
|
render() {
|
|
// Update UI based on state
|
|
this.updateLoadingState();
|
|
this.updateWalletDisplay();
|
|
this.updateFormState();
|
|
this.updateResultsDisplay();
|
|
this.updateErrorDisplay();
|
|
},
|
|
|
|
updateLoadingState() {
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
const loadingState = document.getElementById('loadingState');
|
|
|
|
if (this.data.isLoading) {
|
|
submitBtn.classList.add('btn-loading');
|
|
submitBtn.disabled = true;
|
|
loadingState.style.display = 'block';
|
|
} else {
|
|
submitBtn.classList.remove('btn-loading');
|
|
submitBtn.disabled = false;
|
|
loadingState.style.display = 'none';
|
|
}
|
|
},
|
|
|
|
updateWalletDisplay() {
|
|
const walletElement = document.getElementById('walletBalance');
|
|
if (walletElement) {
|
|
walletElement.textContent = this.data.walletBalance.toFixed(2);
|
|
}
|
|
},
|
|
|
|
updateFormState() {
|
|
// Update form based on state
|
|
},
|
|
|
|
updateResultsDisplay() {
|
|
const resultsContainer = document.getElementById('results');
|
|
if (this.data.results && resultsContainer) {
|
|
// Display results safely
|
|
safeSetHTML(resultsContainer, this.data.results);
|
|
}
|
|
},
|
|
|
|
updateErrorDisplay() {
|
|
const errorContainer = document.getElementById('errorContainer');
|
|
if (errorContainer) {
|
|
errorContainer.innerHTML = '';
|
|
this.data.errors.forEach(error => {
|
|
const errorElement = document.createElement('div');
|
|
errorElement.className = 'alert alert-error';
|
|
errorElement.textContent = error;
|
|
errorContainer.appendChild(errorElement);
|
|
});
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
### Event Management Pattern
|
|
```javascript
|
|
// Event Management Utility
|
|
const EventManager = {
|
|
listeners: new Map(),
|
|
|
|
add(element, event, handler, options = {}) {
|
|
const key = `${element.id || 'unknown'}-${event}`;
|
|
|
|
// Store reference for cleanup
|
|
if (!this.listeners.has(key)) {
|
|
this.listeners.set(key, []);
|
|
}
|
|
|
|
this.listeners.get(key).push({
|
|
element,
|
|
event,
|
|
handler,
|
|
options
|
|
});
|
|
|
|
// Add event listener
|
|
element.addEventListener(event, handler, options);
|
|
},
|
|
|
|
remove(element, event, handler) {
|
|
const key = `${element.id || 'unknown'}-${event}`;
|
|
const listeners = this.listeners.get(key);
|
|
|
|
if (listeners) {
|
|
const index = listeners.findIndex(l =>
|
|
l.element === element &&
|
|
l.event === event &&
|
|
l.handler === handler
|
|
);
|
|
|
|
if (index > -1) {
|
|
listeners.splice(index, 1);
|
|
element.removeEventListener(event, handler);
|
|
}
|
|
}
|
|
},
|
|
|
|
removeAll() {
|
|
this.listeners.forEach(listeners => {
|
|
listeners.forEach(({element, event, handler}) => {
|
|
element.removeEventListener(event, handler);
|
|
});
|
|
});
|
|
|
|
this.listeners.clear();
|
|
},
|
|
|
|
delegate(parent, selector, event, handler) {
|
|
const delegateHandler = (e) => {
|
|
const target = e.target.closest(selector);
|
|
if (target) {
|
|
handler.call(target, e);
|
|
}
|
|
};
|
|
|
|
this.add(parent, event, delegateHandler);
|
|
return delegateHandler;
|
|
}
|
|
};
|
|
```
|
|
|
|
## Performance Optimization Patterns
|
|
|
|
### Lazy Loading
|
|
```javascript
|
|
// Lazy Loading Implementation
|
|
const LazyLoader = {
|
|
observers: new Map(),
|
|
|
|
init() {
|
|
if ('IntersectionObserver' in window) {
|
|
this.createObserver();
|
|
} else {
|
|
// Fallback for older browsers
|
|
this.loadAllContent();
|
|
}
|
|
},
|
|
|
|
createObserver() {
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
this.loadContent(entry.target);
|
|
observer.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, {
|
|
rootMargin: '50px'
|
|
});
|
|
|
|
// Observe elements with lazy loading
|
|
document.querySelectorAll('[data-lazy]').forEach(el => {
|
|
observer.observe(el);
|
|
});
|
|
},
|
|
|
|
loadContent(element) {
|
|
const src = element.dataset.lazy;
|
|
if (src) {
|
|
if (element.tagName === 'IMG') {
|
|
element.src = src;
|
|
} else {
|
|
// Load other content types
|
|
fetch(src)
|
|
.then(response => response.text())
|
|
.then(html => {
|
|
safeSetHTML(element, html);
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
loadAllContent() {
|
|
document.querySelectorAll('[data-lazy]').forEach(el => {
|
|
this.loadContent(el);
|
|
});
|
|
}
|
|
};
|
|
```
|
|
|
|
### Resource Optimization
|
|
```javascript
|
|
// Resource Management
|
|
const ResourceManager = {
|
|
cache: new Map(),
|
|
|
|
// Cache frequently used data
|
|
cache(key, data, ttl = 300000) { // 5 minutes default
|
|
this.cache.set(key, {
|
|
data,
|
|
timestamp: Date.now(),
|
|
ttl
|
|
});
|
|
},
|
|
|
|
// Get cached data
|
|
get(key) {
|
|
const cached = this.cache.get(key);
|
|
if (cached && Date.now() - cached.timestamp < cached.ttl) {
|
|
return cached.data;
|
|
}
|
|
|
|
// Remove expired cache
|
|
this.cache.delete(key);
|
|
return null;
|
|
},
|
|
|
|
// Clear expired cache
|
|
cleanup() {
|
|
const now = Date.now();
|
|
for (const [key, cached] of this.cache) {
|
|
if (now - cached.timestamp >= cached.ttl) {
|
|
this.cache.delete(key);
|
|
}
|
|
}
|
|
},
|
|
|
|
// Preload resources
|
|
preload(urls) {
|
|
urls.forEach(url => {
|
|
const link = document.createElement('link');
|
|
link.rel = 'preload';
|
|
link.href = url;
|
|
link.as = 'fetch';
|
|
document.head.appendChild(link);
|
|
});
|
|
}
|
|
};
|
|
```
|
|
|
|
## Integration Patterns
|
|
|
|
### Django Template Integration
|
|
```html
|
|
<!-- Dynamic Content with Security -->
|
|
<div class="widget" data-widget="wallet">
|
|
<h3>💰 Your Wallet</h3>
|
|
<div class="wallet-balance">
|
|
<span class="balance-amount" id="walletBalance">{{ user.wallet_balance|floatformat:2 }}</span>
|
|
<span class="balance-currency">AED</span>
|
|
</div>
|
|
<a href="{% url 'wallet_topup' %}" class="btn btn-primary btn-sm">
|
|
Top Up Wallet
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Form with CSRF and Validation -->
|
|
<form id="agentForm" method="post" enctype="multipart/form-data" class="agent-form">
|
|
{% csrf_token %}
|
|
|
|
<!-- Dynamic form fields -->
|
|
{% for field in form %}
|
|
<div class="form-group">
|
|
<label for="{{ field.id_for_label }}" class="form-label">
|
|
{{ field.label }}
|
|
{% if field.field.required %}
|
|
<span class="required" aria-label="required">*</span>
|
|
{% endif %}
|
|
</label>
|
|
|
|
{{ field }}
|
|
|
|
{% if field.help_text %}
|
|
<div class="form-help">{{ field.help_text }}</div>
|
|
{% endif %}
|
|
|
|
{% if field.errors %}
|
|
<div class="form-error">
|
|
{% for error in field.errors %}
|
|
{{ error }}
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
|
|
<button type="submit" class="btn btn-primary" id="submitBtn">
|
|
Generate Content
|
|
</button>
|
|
</form>
|
|
```
|
|
|
|
### API Integration Pattern
|
|
```javascript
|
|
// API Communication
|
|
const ApiClient = {
|
|
baseURL: '/api/v1',
|
|
|
|
async request(endpoint, options = {}) {
|
|
const url = `${this.baseURL}${endpoint}`;
|
|
const config = {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': this.getCSRFToken(),
|
|
...options.headers
|
|
},
|
|
...options
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(url, config);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('API request failed:', error);
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
getCSRFToken() {
|
|
return document.querySelector('[name=csrfmiddlewaretoken]')?.value || '';
|
|
},
|
|
|
|
async get(endpoint) {
|
|
return this.request(endpoint, { method: 'GET' });
|
|
},
|
|
|
|
async post(endpoint, data) {
|
|
return this.request(endpoint, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data)
|
|
});
|
|
},
|
|
|
|
async uploadFile(endpoint, formData) {
|
|
return this.request(endpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': this.getCSRFToken()
|
|
// Don't set Content-Type for FormData
|
|
},
|
|
body: formData
|
|
});
|
|
}
|
|
};
|
|
```
|
|
|
|
## Testing Integration
|
|
|
|
### Component Testing
|
|
```javascript
|
|
// Component Test Utilities
|
|
const TestUtils = {
|
|
// Create test element
|
|
createElement(tag, attributes = {}, content = '') {
|
|
const element = document.createElement(tag);
|
|
|
|
Object.entries(attributes).forEach(([key, value]) => {
|
|
element.setAttribute(key, value);
|
|
});
|
|
|
|
if (content) {
|
|
element.textContent = content;
|
|
}
|
|
|
|
return element;
|
|
},
|
|
|
|
// Simulate user interaction
|
|
fireEvent(element, eventType, options = {}) {
|
|
const event = new Event(eventType, {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
...options
|
|
});
|
|
|
|
element.dispatchEvent(event);
|
|
},
|
|
|
|
// Wait for async operations
|
|
waitFor(condition, timeout = 5000) {
|
|
return new Promise((resolve, reject) => {
|
|
const startTime = Date.now();
|
|
|
|
const check = () => {
|
|
if (condition()) {
|
|
resolve();
|
|
} else if (Date.now() - startTime > timeout) {
|
|
reject(new Error('Timeout waiting for condition'));
|
|
} else {
|
|
setTimeout(check, 100);
|
|
}
|
|
};
|
|
|
|
check();
|
|
});
|
|
},
|
|
|
|
// Clean up test DOM
|
|
cleanup() {
|
|
document.querySelectorAll('[data-testid]').forEach(el => {
|
|
el.remove();
|
|
});
|
|
}
|
|
};
|
|
```
|
|
|
|
This architecture ensures consistent, maintainable, and performant templates across all agents while providing the flexibility to customize specific features as needed. |