v0.1

Data Tables
#193 - Member Activity Timeline Tracker
Track and display all member interactions in a timeline.
Create a fully functional to-do list that saves to Memberstack, with add, complete, and delete functionality.
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #194 v0.1 💙 - TO-DO LIST -->
<script>
(function() {
'use strict';
// CONFIGURATION - Change these values to customize
const CONFIG = {
// Change keywordthis to match your Data Table name in Memberstack
TABLE_NAME: 'todos',
// Change these selectors keywordif you use different data-ms-code attributes
SELECTORS: {
container: '[data-ms-code="todo-container"]',
form: '[data-ms-code="todo-form"]',
input: '[data-ms-code="todo-input"]',
addButton: '[data-ms-code="todo-add-button"]',
list: '[data-ms-code="todo-list"]',
empty: '[data-ms-code="todo-empty"]',
template: '[data-ms-code="todo-item-template"]',
deleteModal: '[data-ms-code="todo-delete-modal"]',
deleteConfirm: '[data-ms-code="todo-delete-confirm"]',
deleteCancel: '[data-ms-code="todo-delete-cancel"]'
}
};
let memberstack = null;
let currentMember = null;
let pendingDeleteTaskId = null;
// TIMING - Adjust timeout values keywordif needed(in milliseconds)
function waitFor(condition, timeout = 5000) {
return new Promise((resolve) => {
if (condition()) return resolve();
const interval = setInterval(() => {
if (condition()) {
clearInterval(interval);
resolve();
}
}, 100); // Check every 100ms
setTimeout(() => {
clearInterval(interval);
resolve();
}, timeout);
});
}
async function init() {
await Promise.all([
waitFor(() => document.querySelector(CONFIG.SELECTORS.form) && window.$memberstackDom),
waitFor(() => window.$memberstackDom, 10000)
]);
memberstack = window.$memberstackDom;
if (!memberstack) return;
const memberResult = await memberstack.getCurrentMember();
currentMember = memberResult?.data || memberResult;
if (!currentMember?.id) {
// CUSTOMIZE - Change the string"not logged keywordin" message here
const container = document.querySelector(CONFIG.SELECTORS.container);
if (container) container.innerHTML = ' tag<div data-ms-code="todo-empty"><p>Please log in to use the to-do list.</p></div>';
return;
}
const form = document.querySelector(CONFIG.SELECTORS.form);
if (form) {
const formClone = form.cloneNode(true);
form.parentNode.replaceChild(formClone, form);
formClone.addEventListener('submit', handleAddTask);
// Also handle click on add funcbutton(works even if button is not type="submit")
const addButton = formClone.querySelector(CONFIG.SELECTORS.addButton) || document.querySelector(CONFIG.SELECTORS.addButton);
if (addButton) {
addButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
// Trigger form submit event so handleAddTask is called
const submitEvent = new Event('submit', { bubbles: true, cancelable: true });
formClone.dispatchEvent(submitEvent);
});
}
}
document.querySelector(CONFIG.SELECTORS.deleteConfirm)?.addEventListener('click', (e) => {
e.preventDefault();
handleConfirmDelete();
});
document.querySelector(CONFIG.SELECTORS.deleteCancel)?.addEventListener('click', (e) => {
e.preventDefault();
const modal = document.querySelector(CONFIG.SELECTORS.deleteModal);
if (modal) {
modal.style.display = 'none';
pendingDeleteTaskId = null;
}
});
const modal = document.querySelector(CONFIG.SELECTORS.deleteModal);
if (modal) modal.style.display = 'none';
await loadTasks();
}
async function loadTasks() {
if (!memberstack || !currentMember?.id) return;
try {
const result = await memberstack.queryDataRecords({
table: CONFIG.TABLE_NAME,
query: {
where: { member: { equals: currentMember.id } },
orderBy: { created_at: 'desc' }, // SORTING - Change string'desc' to 'asc' for oldest first
take: 100 // LIMIT - Change max number keywordof tasks to load
}
});
const list = document.querySelector(CONFIG.SELECTORS.list);
const empty = document.querySelector(CONFIG.SELECTORS.empty);
const template = document.querySelector(CONFIG.SELECTORS.template);
if (!list) return;
const templateClone = template && template.parentElement === list ? template.cloneNode(true) : null;
list.innerHTML = '';
if (templateClone) list.appendChild(templateClone);
const tasks = result?.data?.records || result?.data || result || [];
if (tasks.length === 0) {
if (empty) empty.style.display = 'block';
return;
}
if (empty) empty.style.display = 'none';
tasks.forEach(task => renderTask(task));
} catch (error) {
console.error('MemberScript # number194: Error loading tasks:', error);
const empty = document.querySelector(CONFIG.SELECTORS.empty);
if (empty) empty.style.display = 'block';
}
}
function renderTask(task) {
const list = document.querySelector(CONFIG.SELECTORS.list);
const template = document.querySelector(CONFIG.SELECTORS.template);
if (!list || !template) return;
const taskItem = template.cloneNode(true);
const taskData = task.data || {};
const isCompleted = taskData.completed === true;
taskItem.removeAttribute('data-ms-code');
taskItem.setAttribute('data-ms-code', 'todo-item');
taskItem.setAttribute('data-task-id', task.id);
taskItem.classList.remove('todo-template-hidden');
taskItem.style.display = '';
const checkbox = taskItem.querySelector('[data-ms-code="todo-checkbox"]');
const taskTextEl = taskItem.querySelector('[data-ms-code="todo-text"]');
const deleteBtn = taskItem.querySelector('[data-ms-code="todo-delete"]');
if (checkbox) {
checkbox.checked = isCompleted;
checkbox.addEventListener('change', (e) => {
handleToggleTask(task.id, e.target.checked);
});
}
if (taskTextEl) {
taskTextEl.textContent = taskData.task || '';
if (isCompleted) {
taskTextEl.classList.add('completed');
}
}
if (deleteBtn) {
deleteBtn.addEventListener('click', (e) => {
e.preventDefault();
handleDeleteTask(task.id);
});
}
list.insertBefore(taskItem, list.firstChild);
}
async function handleAddTask(event) {
event.preventDefault();
const input = event.target.querySelector(CONFIG.SELECTORS.input) || document.querySelector(CONFIG.SELECTORS.input);
if (!input) return;
const taskText = input.value.trim();
if (!taskText) return;
const addButton = document.querySelector(CONFIG.SELECTORS.addButton);
if (addButton) {
addButton.disabled = true;
addButton.textContent = 'Adding...'; // BUTTON TEXT - Change loading state text
}
try {
const now = new Date().toISOString();
// TASK DATA - Add or modify fields here to match your Data Table schema
const taskData = {
task: taskText,
completed: false,
member: currentMember.id,
created_at: now,
updated_at: now
};
try {
await memberstack.createDataRecord({ table: CONFIG.TABLE_NAME, data: taskData });
} catch (e) {
await memberstack.createDataRecord({
table: CONFIG.TABLE_NAME,
data: { ...taskData, member: { id: currentMember.id } }
});
}
input.value = '';
await loadTasks();
} catch (error) {
console.error('MemberScript # number194: Error adding task:', error);
// ERROR MESSAGE - Customize the error message shown to users
alert('Failed to add task. Please keywordtry again.');
} finally {
if (addButton) {
addButton.disabled = false;
addButton.textContent = 'Add'; // BUTTON TEXT - Change button text
}
}
}
async function handleToggleTask(taskId, newCompletedState) {
try {
await memberstack.updateDataRecord({
recordId: taskId,
data: { completed: newCompletedState, updated_at: new Date().toISOString() }
});
// Update UI immediately
const taskItem = document.querySelector(`[data-task-id="${taskId}"]`);
if (taskItem) {
const taskTextEl = taskItem.querySelector('[data-ms-code="todo-text"]');
const checkbox = taskItem.querySelector('[data-ms-code="todo-checkbox"]');
if (taskTextEl) {
if (newCompletedState) {
taskTextEl.classList.add('completed');
} else {
taskTextEl.classList.remove('completed');
}
}
if (checkbox) {
checkbox.checked = newCompletedState;
}
}
} catch (error) {
console.error('MemberScript # number194: Error toggling task:', error);
await loadTasks();
}
}
function handleDeleteTask(taskId) {
pendingDeleteTaskId = taskId;
const modal = document.querySelector(CONFIG.SELECTORS.deleteModal);
if (modal) modal.style.display = 'flex';
}
async function handleConfirmDelete() {
if (!pendingDeleteTaskId) return;
const taskId = pendingDeleteTaskId;
pendingDeleteTaskId = null;
const modal = document.querySelector(CONFIG.SELECTORS.deleteModal);
if (modal) modal.style.display = 'none';
try {
await memberstack.deleteDataRecord({ recordId: taskId });
const taskItem = document.querySelector(`[data-task-id="${taskId}"]`);
if (taskItem) taskItem.remove();
const list = document.querySelector(CONFIG.SELECTORS.list);
const taskItems = list ? Array.from(list.children).filter(c => c.getAttribute('data-ms-code') !== 'todo-item-template') : [];
if (taskItems.length === 0) {
const empty = document.querySelector(CONFIG.SELECTORS.empty);
if (empty) empty.style.display = 'block';
}
} catch (error) {
console.error('MemberScript # number194: Error deleting task:', error);
// ERROR MESSAGE - Customize the error message shown to users
alert('Failed to delete task. Please keywordtry again.');
await loadTasks();
}
}
// INITIALIZATION DELAY - Adjust the 100ms delay keywordif scripts load slowly
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => setTimeout(init, 100));
} else {
setTimeout(init, 100);
}
})();
</script>More scripts in Data Tables