A complete task management app with priorities, due dates, and role-based editing.Documentation Index
Fetch the complete documentation index at: https://docs.usehasp.com/llms.txt
Use this file to discover all available pages before exploring further.
Schema
Entity: Tasks — key:tasks
| Field | Type | Notes |
|---|---|---|
title | text | required |
description | textarea | |
status | select | open / in_progress / done — required |
priority | select | low / medium / high / critical |
due_date | date | |
assignee_email |
Source Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Tracker</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; color: #1a1a1a; }
h1 { margin-bottom: 20px; }
.controls { display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; }
.controls select, .controls button { padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; }
.controls button { background: #2563eb; color: white; border: none; cursor: pointer; }
.task { border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; margin-bottom: 12px; }
.task h3 { margin-bottom: 4px; }
.task .meta { font-size: 13px; color: #6b7280; display: flex; gap: 12px; margin-top: 8px; flex-wrap: wrap; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 500; }
.badge.open { background: #dbeafe; color: #1e40af; }
.badge.in_progress { background: #fef3c7; color: #92400e; }
.badge.done { background: #d1fae5; color: #065f46; }
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 100; justify-content: center; align-items: center; }
.modal-overlay.open { display: flex; }
.modal { background: white; border-radius: 12px; padding: 24px; width: 100%; max-width: 480px; }
.modal h2 { margin-bottom: 16px; }
.field { margin-bottom: 12px; }
.field label { display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; }
.field input, .field select, .field textarea { width: 100%; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; }
.actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
.actions button { padding: 8px 16px; border-radius: 6px; font-size: 14px; cursor: pointer; }
.btn-cancel { background: white; border: 1px solid #d1d5db; }
.btn-submit { background: #2563eb; color: white; border: none; }
.error { color: #dc2626; font-size: 13px; margin-top: 4px; }
.empty { text-align: center; padding: 40px; color: #9ca3af; }
.task-actions { float: right; display: flex; gap: 4px; }
.task-actions button { padding: 4px 8px; font-size: 12px; border: 1px solid #d1d5db; border-radius: 4px; background: white; cursor: pointer; }
.task-actions button.delete { color: #dc2626; border-color: #fecaca; }
</style>
</head>
<body>
<h1>Task Tracker</h1>
<div class="controls">
<select id="filter-status">
<option value="">All statuses</option>
<option value="open">Open</option>
<option value="in_progress">In Progress</option>
<option value="done">Done</option>
</select>
<button id="btn-create" style="display:none;">+ New Task</button>
</div>
<div id="task-list"><div class="loading">Loading tasks...</div></div>
<div class="modal-overlay" id="modal">
<div class="modal">
<h2 id="modal-title">New Task</h2>
<form id="task-form">
<div class="field">
<label for="title">Title *</label>
<input type="text" id="title" name="title" required>
<div class="error" id="error-title"></div>
</div>
<div class="field">
<label for="description">Description</label>
<textarea id="description" name="description"></textarea>
</div>
<div class="field">
<label for="status">Status *</label>
<select id="status" name="status" required>
<option value="open">Open</option>
<option value="in_progress">In Progress</option>
<option value="done">Done</option>
</select>
</div>
<div class="field">
<label for="priority">Priority</label>
<select id="priority" name="priority">
<option value="">None</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
</div>
<div class="field">
<label for="due_date">Due Date</label>
<input type="date" id="due_date" name="due_date">
</div>
<div class="field">
<label for="assignee_email">Assignee Email</label>
<input type="email" id="assignee_email" name="assignee_email">
</div>
<div class="error" id="error-general"></div>
<div class="actions">
<button type="button" class="btn-cancel" id="btn-cancel">Cancel</button>
<button type="submit" class="btn-submit">Save</button>
</div>
</form>
</div>
</div>
<script src="https://sdk.usehasp.com/v1.js"></script>
<script>
const sdk = new HaspSDK();
let canEdit = false;
let editingId = null;
async function init() {
const bootstrap = await sdk.getBootstrap();
canEdit = ['editor', 'admin'].includes(bootstrap.role);
if (canEdit) document.getElementById('btn-create').style.display = 'block';
loadTasks();
}
async function loadTasks() {
const listEl = document.getElementById('task-list');
const filterStatus = document.getElementById('filter-status').value;
try {
const filter = filterStatus ? { status: { eq: filterStatus } } : undefined;
const result = await sdk.listRecords('tasks', { filter, sort: 'due_date:asc', pageSize: 50 });
if (result.data.length === 0) {
listEl.innerHTML = '<div class="empty">No tasks found.</div>';
return;
}
listEl.innerHTML = result.data.map(task => `
<div class="task">
${canEdit ? `<div class="task-actions">
<button onclick="openEdit('${task.id}')">Edit</button>
<button class="delete" onclick="deleteTask('${task.id}')">Delete</button>
</div>` : ''}
<h3>${escapeHtml(task.title)}</h3>
${task.description ? `<p style="margin-top:6px;color:#374151;">${escapeHtml(task.description)}</p>` : ''}
<div class="meta">
<span class="badge ${task.status}">${task.status.replace('_', ' ')}</span>
${task.priority ? `<span class="badge ${task.priority}">${task.priority}</span>` : ''}
${task.due_date ? `<span>Due: ${task.due_date}</span>` : ''}
</div>
</div>
`).join('');
} catch (error) {
listEl.innerHTML = `<div class="error">Failed to load tasks: ${escapeHtml(error.message)}</div>`;
}
}
document.getElementById('btn-create').addEventListener('click', () => {
editingId = null;
document.getElementById('modal-title').textContent = 'New Task';
document.getElementById('task-form').reset();
document.getElementById('modal').classList.add('open');
});
document.getElementById('btn-cancel').addEventListener('click', () => {
document.getElementById('modal').classList.remove('open');
});
async function openEdit(id) {
const result = await sdk.getRecord('tasks', id);
const task = result.data;
editingId = id;
document.getElementById('modal-title').textContent = 'Edit Task';
document.getElementById('title').value = task.title || '';
document.getElementById('description').value = task.description || '';
document.getElementById('status').value = task.status || 'open';
document.getElementById('priority').value = task.priority || '';
document.getElementById('due_date').value = task.due_date || '';
document.getElementById('assignee_email').value = task.assignee_email || '';
document.getElementById('modal').classList.add('open');
}
document.getElementById('task-form').addEventListener('submit', async (e) => {
e.preventDefault();
clearErrors();
const data = {
title: document.getElementById('title').value,
description: document.getElementById('description').value || null,
status: document.getElementById('status').value,
priority: document.getElementById('priority').value || null,
due_date: document.getElementById('due_date').value || null,
assignee_email: document.getElementById('assignee_email').value || null,
};
try {
if (editingId) {
await sdk.updateRecord('tasks', editingId, data);
} else {
await sdk.createRecord('tasks', data);
}
document.getElementById('modal').classList.remove('open');
loadTasks();
} catch (error) {
const { ErrorCode } = HaspSDK;
if (error.code === ErrorCode.ValidationFailed) {
const fieldErrors = error.getFieldErrors();
Object.entries(fieldErrors).forEach(([field, message]) => {
const el = document.getElementById('error-' + field);
if (el) el.textContent = message;
});
} else {
document.getElementById('error-general').textContent = error.message;
}
}
});
async function deleteTask(id) {
try {
await sdk.deleteRecord('tasks', id);
loadTasks();
} catch (error) {
console.error('Delete failed:', error.message);
}
}
document.getElementById('filter-status').addEventListener('change', loadTasks);
function escapeHtml(str) {
if (str == null) return '';
const div = document.createElement('div');
div.textContent = String(str);
return div.innerHTML;
}
function clearErrors() {
document.querySelectorAll('.error').forEach(el => el.textContent = '');
}
init();
</script>
</body>
</html>
Key Patterns
Delete Confirmations
Always use a custom confirmation dialog before deleting — neverwindow.confirm:
async function deleteTask(id) {
if (!await myConfirmDialog('Delete this task?')) return;
await sdk.deleteRecord('tasks', id);
loadTasks();
}