Skip to main content

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.

A complete task management app with priorities, due dates, and role-based editing.

Schema

Entity: Tasks — key: tasks
FieldTypeNotes
titletextrequired
descriptiontextarea
statusselectopen / in_progress / done — required
priorityselectlow / medium / high / critical
due_datedate
assignee_emailemail

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 — never window.confirm:
async function deleteTask(id) {
  if (!await myConfirmDialog('Delete this task?')) return;
  await sdk.deleteRecord('tasks', id);
  loadTasks();
}