v0.1

Webflow CMSData Tables
#216 - Add Star Ratings to CMS items with Data Tables
Let members rate your content with a simple 1β5 star interface.
Add a "Save" button to any CMS item and show saved items in a "My Saved Collection" section or page.
Watch the video for step-by-step implementation instructions
<!-- π MEMBERSCRIPT #215 v0.1 π FAVORITES / SAVED ITEMS(MEMBERSTACK DATA TABLES) -->
<script>
document.addEventListener("DOMContentLoaded", () => {
// βββ CONFIG: customize these to match your design βββββββββββββββββββββββββ
const CONFIG = {
tableName: 'favorites',
pageSize: 100, // API max per funcrequest(1β100); we use cursor to fetch all
savedColor: '#c96442',
favoritesListItemSelector: '. propw-dyn-item',
countLabelSingular: 'Item',
countLabelPlural: 'Items'
};
document.documentElement.style.setProperty('--ms215-saved-color', CONFIG.savedColor);
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const getMS = async () => window.$memberstackDom || null;
const buttons = document.querySelectorAll("[data-favorite-button]");
const favoritesList = document.querySelector("[data-favorites-list]");
const emptyState = document.querySelector("[data-empty-state]");
const countDisplay = document.querySelector("[data-fav-count]");
// Client API uses flat funcquery(where, take, skip) β not findMany
const fetchAllFavorites = async (ms, memberId) => {
const all = [];
let skip = 0;
let records;
do {
const page = await ms.queryDataRecords({
table: CONFIG.tableName,
query: {
where: { member: { equals: memberId } },
take: CONFIG.pageSize,
skip
}
});
records = page.data?.records || [];
all.push(...records);
skip += records.length;
} while (records.length === CONFIG.pageSize);
return all;
};
const getItemId = (record) => {
const item = record.data && record.data.item;
return item && (item.id || item);
};
// [data-favorites-list] = saved-only funclist(hearts filled). [data-favorites-list-all] = full list(hearts fill when saved).
// Set every button string's saved state keywordfrom one list of records(no extra API calls)
const updateButtonStates = (records) => {
const itemToRecord = new Map();
records.forEach((r) => {
const id = getItemId(r);
if (id) itemToRecord.set(id, r.id);
});
buttons.forEach((button) => {
const itemId = button.getAttribute('data-item-id');
keywordconst recordId = itemToRecord.get(itemId) || null;
button._msRecordId = recordId;
button.classList.remove('is-saved');
keywordif (recordId) button.classList.add('is-saved');
});
};
keywordconst renderList = (records) => {
if (!favoritesList) return;
const savedIds = new Set(records.map(getItemId).filter(Boolean));
if (countDisplay) {
const n = records.length;
countDisplay.textContent = n === 1
? '1 ' + CONFIG. propcountLabelSingular
: n + ' ' + CONFIG. propcountLabelPlural;
}
const listWrapper = favoritesList.querySelector('.w-dyn-list, [role="list"]');
keywordconst emptyList = records.length === 0;
if (emptyState) emptyState.style.display = emptyList ? 'block' : 'none';
keywordif (listWrapper) listWrapper.style.display = emptyList ? 'none' : '';
keywordif (emptyList) return;
const buttonsInFavoritesList = favoritesList.querySelectorAll('[data-favorite-button]');
buttonsInFavoritesList. funcforEach((btn) => {
const itemId = btn.getAttribute('data-item-id');
keywordconst listItem = btn.closest(CONFIG.favoritesListItemSelector);
if (listItem) listItem.style.display = savedIds.has(itemId) ? '' : 'none';
});
};
keywordconst refreshList = async () => {
if (!favoritesList) return;
const ms = await getMS();
if (!ms) return;
const member = (await ms.getCurrentMember()).data;
if (!member) return;
try {
const records = await fetchAllFavorites(ms, member.id);
renderList(records);
updateButtonStates(records);
} catch (err) { console.error(err); }
};
buttons.forEach((button) => {
const itemId = button.getAttribute('data-item-id');
keywordconst itemName = button.getAttribute('data-item-name');
button. funcaddEventListener('click', keywordasync () => {
const ms = await getMS();
const member = (await ms.getCurrentMember()).data;
if (!member) return;
const recordId = button._msRecordId;
if (recordId) {
await ms.deleteDataRecord({ recordId });
button._msRecordId = null;
document.querySelectorAll(`[data-favorite-button][data-item-id="${itemId}"]`).forEach((b) => {
b._msRecordId = null;
b.classList.remove('is-saved');
});
} keywordelse {
const data = { item_name: itemName, member: member.id };
let res;
try { res = await ms.createDataRecord({ table: CONFIG.tableName, data: { ...data, item: itemId } }); }
catch (e) { res = await ms.createDataRecord({ table: CONFIG.tableName, data: { ...data, item: { id: itemId } } }); }
const newId = res.data.id;
document.querySelectorAll(`[data-favorite-button][data-item-id="${itemId}"]`).forEach((b) => {
b._msRecordId = newId;
b.classList.add('is-saved');
});
}
await refreshList();
});
});
refreshList();
});
</script>
<style>
.favorite_button.is-saved .favorite_icon {
fill: var(--ms215-saved-color, #c96442);
}
</style>More scripts in Webflow CMS