Contact Manager

CRUD operations with search, filter, pagination, and form validation

How it works — API Integration

import { signal, computed, effect } from 'tina4js';
import { api } from 'tina4js/api';

// Configure API
api.configure({
  baseUrl: '/api/v1',
  auth: true,
  headers: { 'X-App': 'contacts' }
});

// State
const contacts = signal([]);
const search = signal('');
const deptFilter = signal('');
const page = signal(1);
const perPage = 5;

// Filtered + paginated (computed)
const filtered = computed(() =>
  contacts.value
    .filter(c => !search.value || c.name.toLowerCase().includes(search.value.toLowerCase()))
    .filter(c => !deptFilter.value || c.department === deptFilter.value)
);

const paged = computed(() => {
  const start = (page.value - 1) * perPage;
  return filtered.value.slice(start, start + perPage);
});

// CRUD
async function loadContacts() {
  const data = await api.get('/contacts');
  contacts.value = data;
}

async function createContact(c) {
  const created = await api.post('/contacts', c);
  contacts.value = [...contacts.value, created];
}

async function deleteContact(id) {
  await api.delete(`/contacts/${id}`);
  contacts.value = contacts.value.filter(c => c.id !== id);
}

// Render reacts to signal changes
effect(() => renderTable(paged.value));