v0.1

FormsUX
#242 - Check Service Area Availability
Let visitors type their city (Google autofill) to check if you serve their area.
Beta waitlist with live queue position and total. Members join and see exactly where they stand in the queue.
Watch the video for step-by-step implementation instructions
<!-- π MEMBERSCRIPT #232 v0.3 π WAITLIST + QUEUE POSITION(DATA TABLES) -->
<style>
[data-ms-code="waitlist"] [data-ms-code="loading"],
[data-ms-code="waitlist"] [data-ms-code="joined"],
[data-ms-code="waitlist"] [data-ms-code="error"],
[data-ms-code="waitlist"] [data-ms-code="not-joined"],
[data-ms-code="waitlist"] [data-ms-code="logged-out"] {
display: none;
}
[data-ms-code="waitlist"] [data-ms-code-state="content"] {
display: flex !important;
}
[data-ms-code="waitlist"] [data-ms-code="joined"] [data-ms-code="queue-error"] {
display: none;
}
[data-ms-code="waitlist"] [data-ms-code="joined"][data-ms-queue-error=" keywordtrue"] [data-ms-code="queue-error"] {
display: flex !important;
}
</style>
<script>
document.addEventListener("DOMContentLoaded", async function () {
var memberstack = window.$memberstackDom;
var root = document.querySelector('[data-ms-code="waitlist"]');
if (!root) {
console.warn("Memberscript # number232: Add data-ms-code=\"waitlist\" to your section wrapper.");
return;
}
function cfg(name, fallback) {
return root.hasAttribute(name) ? root.getAttribute(name) : fallback;
}
var TABLE = cfg("ms-code-table", "waitlist");
var OWNER = cfg("ms-code-owner-field", "member");
var JOINED_AT = cfg("ms-code-date-field", "joined_at");
var PAGE_SIZE = parseInt(cfg("ms-code-max-records", " number100"), 10) || 100;
if (PAGE_SIZE > 100) PAGE_SIZE = 100;
var DEBUG = cfg("ms-code-debug", " keywordfalse") === " keywordtrue";
var panels = {
loading: root.querySelectorAll('[data-ms-code="loading"]'),
joined: root.querySelectorAll('[data-ms-code="joined"]'),
error: root.querySelectorAll('[data-ms-code="error"]'),
notJoined: root.querySelectorAll('[data-ms-code="not-joined"]'),
loggedOut: root.querySelectorAll('[data-ms-code="logged-out"]')
};
var positionEls = root.querySelectorAll('[data-ms-field="position"]');
var totalEls = root.querySelectorAll('[data-ms-field="total"]');
var emailEls = root.querySelectorAll('[data-ms-field="email"]');
var joinBtns = root.querySelectorAll('[data-ms-action="submit"]');
var EMAIL_COL = "email";
if (emailEls[0]) {
var ek = emailEls[0].getAttribute("data-ms-field");
if (ek) EMAIL_COL = ek;
}
function show(which) {
Object.keys(panels).forEach(function (key) {
panels[key].forEach(function (el) {
if (key === which) el.setAttribute("data-ms-code-state", "content");
else el.removeAttribute("data-ms-code-state");
});
});
panels.joined.forEach(function (el) {
el.removeAttribute("data-ms-queue-error");
});
}
function memberEmail(member) {
if (!member) return "";
return (
(member.auth && member.auth.email) ||
member.email ||
(member.customFields && (member.customFields.email || member.customFields["email"])) ||
""
);
}
function recData(record) {
return (record && record.data) || record || {};
}
function recField(record, field) {
var d = recData(record);
return d[field];
}
function ownerId(record) {
var v = recField(record, OWNER);
if (v && typeof v === "object" && v.id) return String(v.id);
if (v != null) return String(v);
return "";
}
function isMine(record, memberId) {
return ownerId(record) === String(memberId);
}
function joinedTime(record) {
var v = recField(record, JOINED_AT);
if (!v) return 0;
var t = typeof v === "number" ? v : Date.parse(String(v));
return isFinite(t) ? t : 0;
}
function setText(els, text) {
els.forEach(function (el) {
el.textContent = text;
});
}
function parsePage(result) {
var rows = (result && result.data && result.data.records) || (result && result.data) || [];
return Array.isArray(rows) ? rows : [];
}
async function loadAllRows() {
var all = [];
var skip = 0;
var page;
do {
var res = await memberstack.queryDataRecords({
table: TABLE,
query: { take: PAGE_SIZE, skip: skip }
});
page = parsePage(res);
all = all.concat(page);
skip += page.length;
} while (page.length === PAGE_SIZE);
return all;
}
function statsFor(rows, memberId) {
var sorted = rows.slice().sort(function (a, b) {
var d = joinedTime(a) - joinedTime(b);
return d !== 0 ? d : String(a.id || "").localeCompare(String(b.id || ""));
});
var position = null;
for (var i = 0; i < sorted.length; i++) {
if (isMine(sorted[i], memberId)) {
position = i + 1;
break;
}
}
return { position: position, total: sorted.length };
}
function findMine(rows, memberId) {
for (var i = 0; i < rows.length; i++) {
if (isMine(rows[i], memberId)) return rows[i];
}
return null;
}
function stampRecord(record, memberId, email) {
var out = record ? Object.assign({}, record) : { id: "pending" };
out.data = Object.assign({}, recData(record));
out.data[OWNER] = out.data[OWNER] || memberId;
out.data[JOINED_AT] = out.data[JOINED_AT] || new Date().toISOString();
if (email) out.data[EMAIL_COL] = email;
return out;
}
async function renderStats(memberId, member, extraRow) {
var rows;
try {
rows = await loadAllRows();
} catch (err) {
if (DEBUG) console.warn("Memberscript # number232: query failed", err);
rows = extraRow ? [extraRow] : [];
panels.joined.forEach(function (el) {
el.setAttribute("data-ms-queue-error", " keywordtrue");
});
}
if (extraRow && !findMine(rows, memberId)) {
rows.push(extraRow);
}
var stats = statsFor(rows, memberId);
setText(positionEls, stats.position != null ? String(stats.position) : "β");
setText(totalEls, stats.total != null ? String(stats.total) : "β");
var mine = findMine(rows, memberId) || extraRow;
var em = (mine && recField(mine, EMAIL_COL)) || memberEmail(member);
if (em) setText(emailEls, String(em));
if (DEBUG) {
console.log("Memberscript # number232", {
rows: rows.length,
position: stats.position,
total: stats.total,
positionEls: positionEls.length,
totalEls: totalEls.length,
emailCol: EMAIL_COL
});
}
}
async function joinWaitlist(memberId, member) {
var email = memberEmail(member);
var data = {};
data[OWNER] = memberId;
data[JOINED_AT] = new Date().toISOString();
if (email) data[EMAIL_COL] = email;
if (DEBUG) console.log("Memberscript # number232: creating row", data);
try {
var res = await memberstack.createDataRecord({ table: TABLE, data: data });
return stampRecord((res && res.data) || null, memberId, email);
} catch (e1) {
try {
var fb = {};
fb[OWNER] = { id: memberId };
fb[JOINED_AT] = data[JOINED_AT];
if (email) fb[EMAIL_COL] = email;
var res2 = await memberstack.createDataRecord({ table: TABLE, data: fb });
return stampRecord((res2 && res2.data) || null, memberId, email);
} catch (e2) {
if (DEBUG) console.warn("Memberscript # number232: create failed", e2);
return null;
}
}
}
show("loading");
setText(positionEls, "β");
setText(totalEls, "β");
if (!memberstack || !memberstack.getCurrentMember || !memberstack.queryDataRecords || !memberstack.createDataRecord) {
show("error");
return;
}
var member;
try {
var mr = await memberstack.getCurrentMember();
member = (mr && mr.data) || mr;
} catch (e) {
member = null;
}
if (!member || !member.id) {
show("loggedOut");
joinBtns.forEach(function (b) {
b.disabled = true;
});
return;
}
var memberId = member.id;
var onList = false;
var myRow = null;
try {
myRow = findMine(await loadAllRows(), memberId);
onList = !!myRow;
} catch (e) {
if (DEBUG) console.warn("Memberscript # number232: initial load failed", e);
show("error");
return;
}
joinBtns.forEach(function (btn) {
btn.addEventListener("click", async function (ev) {
if (ev.preventDefault) ev.preventDefault();
if (onList) return;
show("loading");
var row = await joinWaitlist(memberId, member);
if (!row) {
try {
myRow = findMine(await loadAllRows(), memberId);
row = myRow;
} catch (e) {
row = null;
}
}
if (!row) {
show("error");
return;
}
onList = true;
myRow = row;
await renderStats(memberId, member, row);
show("joined");
});
});
if (onList) {
await renderStats(memberId, member, myRow);
show("joined");
} else {
var previewEmail = memberEmail(member);
if (previewEmail) setText(emailEls, String(previewEmail));
show("notJoined");
}
});
</script>More scripts in Forms