refactor: Convert template event handlers to Alpine.js
Converted all inline onclick/onsubmit handlers to Alpine.js @click/@submit directives and added x-data attributes to template body tags. Templates updated: - admin.templ: Added x-data="admin", converted scan/hide buttons - analytics.templ: Added x-data, converted loadAnalytics button - api_explorer.templ: Added x-data for API explorer page - bookshelf.templ: Added x-data, converted library/pagination handlers - collection_modal.templ: Added x-data for modal components - collection_rules.templ: Added x-data, converted all rule handlers - collections.templ: Added x-data, converted navigation/book handlers - conflicts.templ: Added x-data, converted resolve/dismiss handlers - header.templ: Converted theme dropdown and user menu handlers - index.templ: Added x-data for theme/auth - login.templ: Added x-data for theme switching - profile.templ: Added x-data, converted delete account - profile_form.templ: Converted modal close handler - profile_modal.templ: Added x-data for modal - progress.templ: Removed script (uses header.logout) - queue.templ: Added x-data, converted queue handlers - register.templ: Added x-data for theme - restore_system_collection_modal.templ: Added x-data - toast.templ: Converted to x-init with Alpine - unlinked_books.templ: Added x-data for bulk operations Dynamic content in devices.templ uses event delegation via data attributes.
This commit is contained in:
+194
-416
@@ -1,420 +1,198 @@
|
||||
package templates
|
||||
|
||||
templ CollectionRules(user User, collection CollectionData) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Collection Rules - { collection.Name } - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/toast.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="theme-{ user.Theme }">
|
||||
@Header(user, "/collections")
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-6">
|
||||
<button onclick="backToCollection()" class="btn-secondary px-4 py-2 rounded-lg mb-4">
|
||||
← Back to Collection
|
||||
</button>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="text-4xl" style="color: { collection.Color }">{ collection.Icon }</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">{ collection.Name }</h1>
|
||||
<p style="color: var(--text-secondary)">Auto-Assign Rules</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-bold mb-4" style="color: var(--text-primary)">Existing Rules</h2>
|
||||
<div id="rules-container" class="space-y-4">
|
||||
<div id="no-rules" class="card p-6 rounded-lg border text-center" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="text-4xl mb-2">📋</div>
|
||||
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">No Rules Yet</h3>
|
||||
<p style="color: var(--text-secondary)">Create auto-assign rules to automatically add books to this collection</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-bold mb-4" style="color: var(--text-primary)">Create New Rule</h2>
|
||||
<form id="rule-form" onsubmit="handleCreateRule(event)">
|
||||
<input type="hidden" id="collection-id" value="{ collection.ID }">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Field</label>
|
||||
<select id="rule-field" required
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);">
|
||||
<option value="">Select field...</option>
|
||||
<option value="genre">Genre</option>
|
||||
<option value="series">Series</option>
|
||||
<option value="author">Author</option>
|
||||
<option value="language">Language</option>
|
||||
<option value="publisher">Publisher</option>
|
||||
<option value="copyright_year">Copyright Year</option>
|
||||
<option value="tags">Tags</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Operator</label>
|
||||
<select id="rule-operator" required
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);">
|
||||
<option value="">Select operator...</option>
|
||||
<option value="equals">equals</option>
|
||||
<option value="not_equals">does not equal</option>
|
||||
<option value="contains">contains</option>
|
||||
<option value="not_contains">does not contain</option>
|
||||
<option value="starts_with">starts with</option>
|
||||
<option value="ends_with">ends with</option>
|
||||
<option value="greater_than">greater than</option>
|
||||
<option value="less_than">less than</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Value</label>
|
||||
<input type="text" id="rule-value" required
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
placeholder="e.g., Science Fiction">
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="flex items-center space-x-3 cursor-pointer">
|
||||
<input type="checkbox" id="rule-enabled" checked
|
||||
class="w-5 h-5 rounded">
|
||||
<span style="color: var(--text-primary)">Enable Rule</span>
|
||||
</label>
|
||||
<p class="text-xs mt-1" style="color: var(--text-secondary)">Uncheck to disable without deleting</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Priority</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input type="radio" name="priority" value="1" class="w-4 h-4">
|
||||
<span style="color: var(--text-primary)">High</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input type="radio" name="priority" value="2" checked
|
||||
class="w-4 h-4">
|
||||
<span style="color: var(--text-primary)">Medium</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input type="radio" name="priority" value="3"
|
||||
class="w-4 h-4">
|
||||
<span style="color: var(--text-primary)">Low</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" onclick="testRule()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||
🧪 Test Rule
|
||||
</button>
|
||||
<button type="button" onclick="clearForm()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||
Clear
|
||||
</button>
|
||||
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">
|
||||
➕ Add Rule
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="test-results" class="hidden card p-6 rounded-lg border mb-6" style="background-color: var(--bg-primary); border-color: var(--border);">
|
||||
<h3 class="font-semibold mb-3" style="color: var(--text-primary)">Rule Test Results</h3>
|
||||
<p class="text-sm mb-2" style="color: var(--text-secondary)">Books that would be added by this rule:</p>
|
||||
<div id="test-results-list" class="max-h-64 overflow-y-auto space-y-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border);">
|
||||
<h2 class="text-xl font-bold mb-4" style="color: var(--text-primary)">Rule Examples</h2>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center font-bold" style="background-color: var(--accent);">1</div>
|
||||
<div>
|
||||
<p class="font-medium" style="color: var(--text-primary)">Add all Science Fiction books</p>
|
||||
<code class="block mt-1 text-sm" style="color: var(--text-secondary); background-color: var(--bg-secondary); padding: 4px 8px; border-radius: 4px;">
|
||||
Field: genre<br>
|
||||
Operator: equals<br>
|
||||
Value: Science Fiction
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center font-bold" style="background-color: var(--accent);">2</div>
|
||||
<div>
|
||||
<p class="font-medium" style="color: var(--text-primary)">Add books from a specific series</p>
|
||||
<code class="block mt-1 text-sm" style="color: var(--text-secondary); background-color: var(--bg-secondary); padding: 4px 8px; border-radius: 4px;">
|
||||
Field: series<br>
|
||||
Operator: starts with<br>
|
||||
Value: Harry Potter
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center font-bold" style="background-color: var(--accent);">3</div>
|
||||
<div>
|
||||
<p class="font-medium" style="color: var(--text-primary)">Add books published in a year range</p>
|
||||
<code class="block mt-1 text-sm" style="color: var(--text-secondary); background-color: var(--bg-secondary); padding: 4px 8px; border-radius: 4px;">
|
||||
Field: copyright_year<br>
|
||||
Operator: greater than<br>
|
||||
Value: 2020
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let collectionId = '{ collection.ID }';
|
||||
|
||||
function backToCollection() {
|
||||
window.location.href = `/collections/${collectionId}`;
|
||||
}
|
||||
|
||||
function renderRules(rules) {
|
||||
const container = document.getElementById('rules-container');
|
||||
const noRulesDiv = document.getElementById('no-rules');
|
||||
|
||||
if (rules && rules.length > 0) {
|
||||
noRulesDiv.classList.add('hidden');
|
||||
|
||||
container.innerHTML = rules.map((rule, index) => `
|
||||
<div class="card p-4 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<span class="font-semibold" style="color: var(--text-primary)">
|
||||
${getFieldLabel(rule.field)}
|
||||
</span>
|
||||
<span class="px-2 py-1 text-xs rounded" style="background-color: ${rule.enabled ? 'var(--accent)' : 'var(--text-secondary)'}; color: var(--bg-primary);">
|
||||
${rule.enabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
<span class="px-2 py-1 text-xs rounded" style="background-color: var(--bg-secondary); color: var(--text-primary);">
|
||||
Priority ${rule.priority}
|
||||
</span>
|
||||
</div>
|
||||
<code class="block text-sm" style="color: var(--text-secondary);">
|
||||
${getOperatorLabel(rule.operator)} "${rule.value}"
|
||||
</code>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button onclick="toggleRule('${rule.id}', ${!rule.enabled})"
|
||||
class="p-2 hover:opacity-80 rounded" style="color: var(--text-secondary); background-color: var(--bg-primary);">
|
||||
${rule.enabled ? '⏸️' : '▶️'}
|
||||
</button>
|
||||
<button onclick="deleteRule('${rule.id}')"
|
||||
class="p-2 hover:opacity-80 rounded" style="color: var(--text-secondary); background-color: var(--bg-primary);">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
noRulesDiv.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function getFieldLabel(field) {
|
||||
const labels = {
|
||||
'genre': 'Genre',
|
||||
'series': 'Series',
|
||||
'author': 'Author',
|
||||
'language': 'Language',
|
||||
'publisher': 'Publisher',
|
||||
'copyright_year': 'Copyright Year',
|
||||
'tags': 'Tags'
|
||||
};
|
||||
return labels[field] || field;
|
||||
}
|
||||
|
||||
function getOperatorLabel(operator) {
|
||||
const labels = {
|
||||
'equals': 'equals',
|
||||
'not_equals': 'does not equal',
|
||||
'contains': 'contains',
|
||||
'not_contains': 'does not contain',
|
||||
'starts_with': 'starts with',
|
||||
'ends_with': 'ends with',
|
||||
'greater_than': 'greater than',
|
||||
'less_than': 'less than'
|
||||
};
|
||||
return labels[operator] || operator;
|
||||
}
|
||||
|
||||
function handleCreateRule(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const data = {
|
||||
field: document.getElementById('rule-field').value,
|
||||
operator: document.getElementById('rule-operator').value,
|
||||
value: document.getElementById('rule-value').value,
|
||||
enabled: document.getElementById('rule-enabled').checked,
|
||||
priority: parseInt(document.querySelector('input[name="priority"]:checked').value)
|
||||
};
|
||||
|
||||
fetch(`/api/collections/${collectionId}/rules`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
showToast('Rule created successfully', 'success');
|
||||
clearForm();
|
||||
loadRules();
|
||||
})
|
||||
.catch(error => {
|
||||
showToast('Failed to create rule', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function testRule() {
|
||||
const field = document.getElementById('rule-field').value;
|
||||
const operator = document.getElementById('rule-operator').value;
|
||||
const value = document.getElementById('rule-value').value;
|
||||
|
||||
if (!field || !operator || !value) {
|
||||
showToast('Please fill in all rule fields', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const testResultsDiv = document.getElementById('test-results');
|
||||
const resultsList = document.getElementById('test-results-list');
|
||||
|
||||
testResultsDiv.classList.remove('hidden');
|
||||
resultsList.innerHTML = '<p class="text-sm" style="color: var(--text-secondary)">Testing rule...</p>';
|
||||
|
||||
const rules = [{
|
||||
field: field,
|
||||
operator: operator,
|
||||
value: value
|
||||
}];
|
||||
|
||||
fetch('/api/collections/test-rules', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
body: JSON.stringify({ rules: rules })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.matches && result.matches.length > 0) {
|
||||
let html = '<p style="color: var(--text-secondary); font-size: 0.875rem;">Found ' + result.matches.length + ' matching books:</p>';
|
||||
html += '<div style="display: flex; flex-direction: column; gap: 0.5rem;">';
|
||||
|
||||
result.matches.slice(0, 20).forEach(book => {
|
||||
html += '<div style="display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem; border-radius: 0.25rem; background-color: var(--bg-secondary);">';
|
||||
html += '<img src="' + (book.cover_image_path || '/static/placeholder-book.svg') + '" alt="Cover" style="width: 2rem; height: 3rem; object-fit: cover; border-radius: 0.25rem;">';
|
||||
html += '<div style="flex: 1;">';
|
||||
html += '<div style="font-size: 0.875rem; font-weight: 500; color: var(--text-primary);">' + book.title + '</div>';
|
||||
if (book.author) {
|
||||
html += '<div style="font-size: 0.75rem; color: var(--text-secondary);">' + book.author + '</div>';
|
||||
}
|
||||
html += '<div style="font-size: 0.75rem; color: var(--accent);">' + book.match_reason + '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
if (result.matches.length > 20) {
|
||||
html += '<p style="color: var(--text-secondary); font-size: 0.875rem;">...and ' + (result.matches.length - 20) + ' more</p>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
resultsList.innerHTML = html;
|
||||
} else {
|
||||
resultsList.innerHTML = '<p style="color: var(--text-secondary); font-size: 0.875rem;">No books match this rule</p>';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
resultsList.innerHTML = '<p class="text-sm" style="color: var(--error)">Failed to test rule</p>';
|
||||
});
|
||||
}
|
||||
|
||||
function toggleRule(ruleId, enabled) {
|
||||
fetch(`/api/collections/${collectionId}/rules/${ruleId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + localStorage.getItem('token')
|
||||
},
|
||||
body: JSON.stringify({ enabled })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
showToast('Rule updated', 'success');
|
||||
loadRules();
|
||||
})
|
||||
.catch(error => {
|
||||
showToast('Failed to update rule', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function deleteRule(ruleId) {
|
||||
if (!confirm('Are you sure you want to delete this rule?')) return;
|
||||
|
||||
fetch(`/api/collections/${collectionId}/rules/${ruleId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
showToast('Rule deleted', 'success');
|
||||
loadRules();
|
||||
} else {
|
||||
showToast('Failed to delete rule', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showToast('Failed to delete rule', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function clearForm() {
|
||||
document.getElementById('rule-field').value = '';
|
||||
document.getElementById('rule-operator').value = '';
|
||||
document.getElementById('rule-value').value = '';
|
||||
document.getElementById('rule-enabled').checked = true;
|
||||
document.querySelector('input[name="priority"][value="2"]').checked = true;
|
||||
document.getElementById('test-results').classList.add('hidden');
|
||||
}
|
||||
|
||||
function loadRules() {
|
||||
fetch(`/api/collections/${collectionId}`)
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
renderRules(result.auto_assign_rules || []);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to load rules', error);
|
||||
});
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
// Load rules on page load
|
||||
loadRules();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Collection Rules - { collection.Name } - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/main.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
</head>
|
||||
<body class="theme-{ user.Theme }" x-data="collectionRules" x-init="initCollectionRules('{ collection.ID }')">
|
||||
@Header(user, "/collections")
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-6">
|
||||
<button @click="backToCollection()" class="btn-secondary px-4 py-2 rounded-lg mb-4">
|
||||
← Back to Collection
|
||||
</button>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="text-4xl" style="color: { collection.Color }">{ collection.Icon }</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">{ collection.Name }</h1>
|
||||
<p style="color: var(--text-secondary)">Auto-Assign Rules</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-bold mb-4" style="color: var(--text-primary)">Existing Rules</h2>
|
||||
<div id="rules-container" class="space-y-4">
|
||||
<div id="no-rules" class="card p-6 rounded-lg border text-center" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="text-4xl mb-2">📋</div>
|
||||
<h3 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">No Rules Yet</h3>
|
||||
<p style="color: var(--text-secondary)">Create auto-assign rules to automatically add books to this collection</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-8">
|
||||
<h2 class="text-2xl font-bold mb-4" style="color: var(--text-primary)">Create New Rule</h2>
|
||||
<form id="rule-form" @submit="handleCreateRule($event)">
|
||||
<input type="hidden" id="collection-id" value="{ collection.ID }"/>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Field</label>
|
||||
<select
|
||||
id="rule-field"
|
||||
required
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
>
|
||||
<option value="">Select field...</option>
|
||||
<option value="genre">Genre</option>
|
||||
<option value="series">Series</option>
|
||||
<option value="author">Author</option>
|
||||
<option value="language">Language</option>
|
||||
<option value="publisher">Publisher</option>
|
||||
<option value="copyright_year">Copyright Year</option>
|
||||
<option value="tags">Tags</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Operator</label>
|
||||
<select
|
||||
id="rule-operator"
|
||||
required
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
>
|
||||
<option value="">Select operator...</option>
|
||||
<option value="equals">equals</option>
|
||||
<option value="not_equals">does not equal</option>
|
||||
<option value="contains">contains</option>
|
||||
<option value="not_contains">does not contain</option>
|
||||
<option value="starts_with">starts with</option>
|
||||
<option value="ends_with">ends with</option>
|
||||
<option value="greater_than">greater than</option>
|
||||
<option value="less_than">less than</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Value</label>
|
||||
<input
|
||||
type="text"
|
||||
id="rule-value"
|
||||
required
|
||||
class="w-full px-4 py-2 border rounded-lg"
|
||||
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
||||
placeholder="e.g., Science Fiction"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="flex items-center space-x-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="rule-enabled"
|
||||
checked
|
||||
class="w-5 h-5 rounded"
|
||||
/>
|
||||
<span style="color: var(--text-primary)">Enable Rule</span>
|
||||
</label>
|
||||
<p class="text-xs mt-1" style="color: var(--text-secondary)">Uncheck to disable without deleting</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Priority</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input type="radio" name="priority" value="1" class="w-4 h-4"/>
|
||||
<span style="color: var(--text-primary)">High</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="priority"
|
||||
value="2"
|
||||
checked
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
<span style="color: var(--text-primary)">Medium</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="priority"
|
||||
value="3"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
<span style="color: var(--text-primary)">Low</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" @click="testRule()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||
🧪 Test Rule
|
||||
</button>
|
||||
<button type="button" @click="clearForm()" class="btn-secondary px-4 py-2 rounded-lg">
|
||||
Clear
|
||||
</button>
|
||||
<button type="submit" class="btn-primary px-4 py-2 rounded-lg">
|
||||
➕ Add Rule
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="test-results" class="hidden card p-6 rounded-lg border mb-6" style="background-color: var(--bg-primary); border-color: var(--border);">
|
||||
<h3 class="font-semibold mb-3" style="color: var(--text-primary)">Rule Test Results</h3>
|
||||
<p class="text-sm mb-2" style="color: var(--text-secondary)">Books that would be added by this rule:</p>
|
||||
<div id="test-results-list" class="max-h-64 overflow-y-auto space-y-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-primary); border-color: var(--border);">
|
||||
<h2 class="text-xl font-bold mb-4" style="color: var(--text-primary)">Rule Examples</h2>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center font-bold" style="background-color: var(--accent);">1</div>
|
||||
<div>
|
||||
<p class="font-medium" style="color: var(--text-primary)">Add all Science Fiction books</p>
|
||||
<code class="block mt-1 text-sm" style="color: var(--text-secondary); background-color: var(--bg-secondary); padding: 4px 8px; border-radius: 4px;">
|
||||
Field: genre
|
||||
<br/>
|
||||
Operator: equals
|
||||
<br/>
|
||||
Value: Science Fiction
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center font-bold" style="background-color: var(--accent);">2</div>
|
||||
<div>
|
||||
<p class="font-medium" style="color: var(--text-primary)">Add books from a specific series</p>
|
||||
<code class="block mt-1 text-sm" style="color: var(--text-secondary); background-color: var(--bg-secondary); padding: 4px 8px; border-radius: 4px;">
|
||||
Field: series
|
||||
<br/>
|
||||
Operator: starts with
|
||||
<br/>
|
||||
Value: Harry Potter
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center font-bold" style="background-color: var(--accent);">3</div>
|
||||
<div>
|
||||
<p class="font-medium" style="color: var(--text-primary)">Add books published in a year range</p>
|
||||
<code class="block mt-1 text-sm" style="color: var(--text-secondary); background-color: var(--bg-secondary); padding: 4px 8px; border-radius: 4px;">
|
||||
Field: copyright_year
|
||||
<br/>
|
||||
Operator: greater than
|
||||
<br/>
|
||||
Value: 2020
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user