Files
LogWhispererAI/.opencode/skills/examples/modal-implementation.md
Luca Sacchi Ricciardi aa489c7eb8 docs: add comprehensive frontend landing page plan and download design skills
Add detailed landing page development plan in docs/frontend_landing_plan.md:
- Complete landing page structure (Hero, Problem/Solution, Features, Demo, CTA)
- Design guidelines from downloaded skills (typography, color, motion, composition)
- Security considerations (XSS prevention, input sanitization, CSP)
- Performance targets (LCP <2.5s, bundle <150KB, Lighthouse >90)
- Responsiveness and accessibility requirements (WCAG 2.1 AA)
- Success KPIs and monitoring setup
- 3-week development timeline with daily tasks
- Definition of Done checklist

Download 10+ frontend/UI/UX skills via universal-skills-manager:
- frontend-ui-ux: UI/UX design without mockups
- frontend-design-guidelines: Production-grade interface guidelines
- frontend-developer: React best practices (40+ rules)
- frontend-engineer: Next.js 14 App Router patterns
- ui-ux-master: Comprehensive design systems and accessibility
- ui-ux-systems-designer: Information architecture and interaction
- ui-ux-design-user-experience: Platform-specific guidelines
- Plus additional reference materials and validation scripts

Configure universal-skills MCP with SkillsMP API key for curated skill access.

Safety first: All skills validated before installation, no project code modified.

Refs: Universal Skills Manager (github:jacob-bd/universal-skills-manager)
Next: Begin Sprint 3 landing page development
2026-04-03 13:13:59 +02:00

425 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Modal Implementation Example: Transaction Modal
Реальный пример из Family Budget: модальное окно для создания транзакции.
## Files
- **Template**: `frontend/web/templates/components/modal_transaction.html`
- **JavaScript**: `frontend/web/static/js/budget/transactionForm.js`
- **Usage**: `frontend/web/templates/index.html`
## Architecture
```
┌─────────────────────────────────────────────────────────┐
│ index.html (parent template) │
│ │
│ {% from "components/modal_transaction.html" import │
│ transaction_modal %} │
│ │
│ {{ transaction_modal(modal_id='modal_add_transaction') }}│
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ modal_transaction.html (Jinja2 macro) │
│ │
│ <dialog id="{{ modal_id }}" class="modal"> │
│ <div class="modal-box"> │
│ <form id="form_{{ modal_id }}"> │
│ <!-- Form fields --> │
│ </form> │
│ </div> │
│ </dialog> │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ transactionForm.js (CRUD logic) │
│ │
│ - saveTransaction() - POST /api/v1/facts │
│ - updateTransaction() - PUT /api/v1/facts/{id} │
│ - deleteTransaction() - DELETE /api/v1/facts/{id} │
│ - Offline sync via IndexedDB │
│ - WebSocket broadcast refresh │
└─────────────────────────────────────────────────────────┘
```
## Implementation
### 1. Jinja2 Macro (modal_transaction.html)
```jinja2
{% macro transaction_modal(modal_id='modal_add_transaction') %}
<dialog id="{{ modal_id }}" class="modal">
<div class="modal-box w-full max-w-2xl p-4 max-h-[90vh] overflow-y-auto my-auto">
<h3 class="font-bold text-lg mb-3"> Добавить факт</h3>
<form id="form_{{ modal_id }}" class="space-y-2">
<!-- Дата -->
<div class="form-control">
<label class="label py-1">
<span class="label-text">Дата *</span>
</label>
<!-- Quick date buttons -->
<div class="flex gap-1 mb-1">
<button type="button" class="btn btn-xs btn-outline flex-1"
onclick="setTransactionDate(0)">Сегодня</button>
<button type="button" class="btn btn-xs btn-outline flex-1"
onclick="setTransactionDate(-1)">Вчера</button>
<button type="button" class="btn btn-xs btn-outline flex-1"
onclick="setTransactionDate(-2)">Позавчера</button>
</div>
<input type="text" name="fact_date" required
class="input input-bordered w-full transaction-date-input"
placeholder="ДД.ММ.ГГГГ" maxlength="10" />
</div>
<!-- Счет (Financial Center) -->
<div class="form-control">
<label class="label py-1">
<span class="label-text">Счет *</span>
</label>
<select name="financial_center_id" required class="select select-bordered">
<option value="">-- Выберите счет --</option>
</select>
</div>
<!-- Тип операции (Radio buttons as DaisyUI buttons) -->
<div class="form-control">
<label class="label py-1">
<span class="label-text font-semibold">Тип операции *</span>
</label>
<div class="grid grid-cols-2 gap-2">
<label class="btn btn-sm btn-outline btn-error transaction-type-btn btn-active"
data-type="expense">
<input type="radio" name="record_type" value="expense" class="hidden" checked />
Расход
</label>
<label class="btn btn-sm btn-outline btn-success transaction-type-btn"
data-type="income">
<input type="radio" name="record_type" value="income" class="hidden" />
Доход
</label>
</div>
</div>
<!-- Категория (Article) - filtered by Financial Center -->
<div class="form-control">
<label class="label py-1">
<span class="label-text">Категория *</span>
</label>
<select name="article_id" required class="select select-bordered">
<option value="" disabled hidden>-- Выберите категорию --</option>
</select>
</div>
<!-- Сумма -->
<div class="form-control">
<label class="label py-1">
<span class="label-text">Сумма *</span>
</label>
<input type="number" name="amount" step="1" min="1" required
class="input input-bordered" placeholder="0" />
</div>
<!-- Описание -->
<div class="form-control">
<label class="label py-1">
<span class="label-text">Описание</span>
</label>
<textarea name="description" class="textarea textarea-bordered"
placeholder="Комментарий" rows="1"></textarea>
</div>
<!-- Modal Actions -->
<div class="modal-action mt-3">
<button type="button" onclick="{{ modal_id }}.close()" class="btn btn-sm btn-ghost">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
Отмена
</button>
<!-- Unified save button (green online, orange offline) -->
<button type="button" class="btn btn-sm save-btn"
data-form-id="form_{{ modal_id }}"
data-modal-id="{{ modal_id }}"
onclick="saveTransaction(this)"
title="Сохранить">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Сохранить
</button>
</div>
</form>
</div>
<!-- Click backdrop to close -->
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
{% endmacro %}
```
### 2. JavaScript Logic (transactionForm.js)
```javascript
/**
* Save transaction (CREATE or UPDATE)
* Supports online/offline modes
*/
async function saveTransaction(button) {
const formId = button.dataset.formId;
const modalId = button.dataset.modalId;
const form = document.getElementById(formId);
const modal = document.getElementById(modalId);
// Validate form
if (!form.checkValidity()) {
form.reportValidity();
return;
}
// Get form data
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
// Convert types
data.amount = parseFloat(data.amount);
data.financial_center_id = parseInt(data.financial_center_id);
data.article_id = parseInt(data.article_id);
if (data.cost_center_id) {
data.cost_center_id = parseInt(data.cost_center_id);
}
try {
// Check if online or offline
if (navigator.onLine) {
// Online: POST to API
const response = await fetch('/api/v1/facts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to save transaction');
}
const result = await response.json();
console.log('Transaction saved:', result.id);
// Close modal
modal.close();
// Refresh data via HTMX
htmx.trigger('#recent-transactions', 'refresh');
htmx.trigger('#quick-stats', 'refresh');
// Show success toast
showToast('Транзакция сохранена', 'success');
} else {
// Offline: Save to IndexedDB
await window.offlineSync.queueTransaction(data);
// Close modal
modal.close();
// Show offline toast
showToast('Сохранено offline (синхронизируется при подключении)', 'warning');
}
} catch (error) {
console.error('Save transaction error:', error);
showToast(error.message || 'Ошибка сохранения', 'error');
}
}
/**
* Open modal for creating new transaction
*/
function openAddTransactionModal() {
const modal = document.getElementById('modal_add_transaction');
const form = document.getElementById('form_modal_add_transaction');
// Reset form
form.reset();
// Set default date (today)
setTransactionDate(0);
// Set default type (expense)
document.querySelector('input[name="record_type"][value="expense"]').checked = true;
document.querySelectorAll('.transaction-type-btn').forEach(btn => {
btn.classList.remove('btn-active');
});
document.querySelector('.transaction-type-btn[data-type="expense"]').classList.add('btn-active');
// Load Financial Centers and Articles
loadFormOptions();
// Open modal
modal.showModal();
}
/**
* Set transaction date
* @param {number} daysOffset - Days offset from today (0=today, -1=yesterday, etc.)
*/
function setTransactionDate(daysOffset) {
const date = new Date();
date.setDate(date.getDate() + daysOffset);
const dateStr = date.toLocaleDateString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
const input = document.querySelector('.transaction-date-input');
if (input) {
input.value = dateStr;
}
}
/**
* Load Financial Centers and Articles for form
*/
async function loadFormOptions() {
try {
// Load Financial Centers
const fcResponse = await fetch('/api/v1/financial-centers');
const financialCenters = await fcResponse.json();
const fcSelect = document.querySelector('select[name="financial_center_id"]');
fcSelect.innerHTML = '<option value="">-- Выберите счет --</option>';
financialCenters.forEach(fc => {
fcSelect.innerHTML += `<option value="${fc.id}">${fc.name}</option>`;
});
// Load Articles (filtered by Financial Center after selection)
const articlesResponse = await fetch('/api/v1/articles');
const articles = await articlesResponse.json();
const articleSelect = document.querySelector('select[name="article_id"]');
articleSelect.innerHTML = '<option value="">-- Выберите категорию --</option>';
articles.forEach(article => {
articleSelect.innerHTML += `<option value="${article.id}">${article.name}</option>`;
});
} catch (error) {
console.error('Load form options error:', error);
showToast('Ошибка загрузки справочников', 'error');
}
}
```
### 3. Usage in Template
```jinja2
{% extends "base.html" %}
{% from "components/modal_transaction.html" import transaction_modal %}
{% block content %}
<!-- Page content -->
<div class="space-y-6">
<!-- Quick actions -->
<button onclick="openAddTransactionModal()" class="btn btn-success">
Добавить факт
</button>
<!-- Recent transactions -->
<div id="recent-transactions" hx-get="/api/v1/facts/recent" hx-trigger="load">
<!-- Loaded via HTMX -->
</div>
</div>
<!-- Modal (outside content div) -->
{{ transaction_modal(modal_id='modal_add_transaction') }}
{% endblock %}
```
## Key Features
### 1. DaisyUI Modal
```html
<dialog id="modal_id" class="modal">
<div class="modal-box">...</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
```
- Native `<dialog>` element (HTML5)
- DaisyUI classes: `modal`, `modal-box`, `modal-backdrop`
- JavaScript API: `modal.showModal()`, `modal.close()`
### 2. Form Validation
- HTML5 validation: `required`, `min`, `maxlength`
- JavaScript: `form.checkValidity()`, `form.reportValidity()`
- Custom validation in `saveTransaction()`
### 3. Offline Support
- Check `navigator.onLine`
- Save to IndexedDB when offline
- Sync queue processed when back online
- Visual feedback (orange button when offline)
### 4. HTMX Integration
```javascript
// Refresh HTMX content after save
htmx.trigger('#recent-transactions', 'refresh');
htmx.trigger('#quick-stats', 'refresh');
```
### 5. Radio Buttons as Buttons
```html
<label class="btn btn-outline btn-error transaction-type-btn">
<input type="radio" name="record_type" value="expense" class="hidden" />
Расход
</label>
```
- Styled as DaisyUI buttons
- Hidden radio input
- Toggle `btn-active` class on click
## Performance
- **Modal open**: <100ms (minimal JavaScript)
- **Form load**: ~200ms (2 API calls in parallel)
- **Save**: ~150ms (POST + HTMX refresh)
- **Offline save**: ~50ms (IndexedDB write)
## Accessibility
- ✅ Keyboard navigation (Tab, Enter, Esc)
- ✅ ARIA labels (implicit from `<label>` elements)
- ✅ Focus management (auto-focus first input when opened)
- ✅ Screen reader compatible (DaisyUI semantic HTML)
## Browser Support
- ✅ Chrome/Edge (full support)
- ✅ Firefox (full support)
- ✅ Safari (full support, including iOS)
- ✅ Mobile browsers (responsive design)
## References
- DaisyUI Modal: https://daisyui.com/components/modal/
- HTMX: https://htmx.org/
- HTML Dialog Element: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog