memberstack-ai-context
Version:
AI context server for Memberstack DOM documentation - provides intelligent access to Memberstack docs for Claude Code, Cursor, and other AI coding assistants
1,008 lines (844 loc) • 27.6 kB
Markdown
# Memberstack DOM - Advanced Features
## AI Assistant Instructions
When implementing advanced Memberstack features:
- Use `getSecureContent()` for plan-gated content protection
- Implement comments with `createPost()`, `createThread()`, `getPosts()`, `getThreads()`
- Use team methods: `joinTeam()`, `getTeam()`, `generateInviteToken()`
- Include proper authentication checks before advanced operations
- Handle real-time features with WebSocket connections for comments
- Show loading states for content fetching operations
## Overview
Advanced Memberstack DOM features include secure content delivery, comments system, team management, and plan-based access control. These features enable complex membership applications with rich user interactions.
## Secure Content
### getSecureContent()
Retrieve plan-protected content that's only accessible to members with specific subscriptions.
**Method Signature:**
```typescript
await memberstack.getSecureContent({
contentId: string;
}): Promise<GetSecureContentPayload>
```
**Parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| contentId | string | ✅ | Unique identifier for the secure content |
**Response:**
```typescript
{
data: {
id: string;
content: string;
contentType: "HTML" | "TEXT" | "JSON" | "MARKDOWN";
accessLevel: string;
// ... additional content properties
}
}
```
**Examples:**
Basic Secure Content Retrieval:
```javascript
async function loadSecureContent(contentId) {
try {
const result = await memberstack.getSecureContent({
contentId: contentId
});
console.log('Secure content loaded:', result.data);
return result.data;
} catch (error) {
console.error('Failed to load secure content:', error);
// Handle different error scenarios
if (error.code === 'INSUFFICIENT_ACCESS') {
throw new Error('This content requires a premium subscription');
} else if (error.code === 'CONTENT_NOT_FOUND') {
throw new Error('Content not found');
} else {
throw new Error('Failed to load content');
}
}
}
// Usage
document.getElementById('load-content-btn').addEventListener('click', async () => {
try {
const content = await loadSecureContent('premium-tutorial-123');
document.getElementById('content-area').innerHTML = content.content;
} catch (error) {
document.getElementById('content-error').textContent = error.message;
document.getElementById('content-error').style.display = 'block';
}
});
```
Content Gate Manager:
```javascript
class SecureContentManager {
constructor() {
this.memberstack = window.$memberstackDom;
this.contentCache = new Map();
this.init();
}
async init() {
this.setupContentGates();
this.setupDynamicLoading();
}
setupContentGates() {
// Find all secure content elements
document.querySelectorAll('[data-secure-content]').forEach(element => {
this.setupContentGate(element);
});
}
async setupContentGate(element) {
const contentId = element.dataset.secureContent;
const requiredPlan = element.dataset.requiredPlan;
// Check if member has required access
const hasAccess = await this.checkAccess(requiredPlan);
if (hasAccess) {
await this.loadSecureContentIntoElement(element, contentId);
} else {
this.showAccessDeniedMessage(element, requiredPlan);
}
}
async checkAccess(requiredPlan) {
try {
const member = await this.memberstack.getCurrentMember();
if (!member.data) {
return false;
}
// Check if member has the required plan
return member.data.planConnections?.some(connection =>
connection.planId === requiredPlan && connection.status === 'ACTIVE'
);
} catch (error) {
console.error('Failed to check access:', error);
return false;
}
}
async loadSecureContentIntoElement(element, contentId) {
try {
// Show loading state
element.innerHTML = '<div class="loading">Loading premium content...</div>';
// Check cache first
let content = this.contentCache.get(contentId);
if (!content) {
const result = await this.memberstack.getSecureContent({ contentId });
content = result.data;
this.contentCache.set(contentId, content);
}
// Render content based on type
this.renderContent(element, content);
} catch (error) {
console.error('Failed to load secure content:', error);
this.showContentError(element, error.message);
}
}
renderContent(element, content) {
switch (content.contentType) {
case 'HTML':
element.innerHTML = content.content;
break;
case 'MARKDOWN':
// Assume a markdown parser is available
element.innerHTML = this.parseMarkdown(content.content);
break;
case 'JSON':
const data = JSON.parse(content.content);
element.innerHTML = this.renderJSONContent(data);
break;
case 'TEXT':
default:
element.textContent = content.content;
break;
}
element.classList.add('secure-content-loaded');
}
showAccessDeniedMessage(element, requiredPlan) {
element.innerHTML = `
<div class="access-denied">
<div class="lock-icon">🔒</div>
<h3>Premium Content</h3>
<p>This content is only available to ${requiredPlan} subscribers.</p>
<div class="access-actions">
<button onclick="this.showUpgradeModal('${requiredPlan}')" class="upgrade-btn">
Upgrade Now
</button>
<button onclick="memberstack.openModal('LOGIN')" class="login-btn">
Sign In
</button>
</div>
</div>
`;
element.classList.add('access-denied');
}
showContentError(element, message) {
element.innerHTML = `
<div class="content-error">
<p>Failed to load content: ${message}</p>
<button onclick="location.reload()">Retry</button>
</div>
`;
}
setupDynamicLoading() {
// Progressive content loading on scroll
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const element = entry.target;
const contentId = element.dataset.secureContent;
if (contentId && !element.classList.contains('secure-content-loaded')) {
this.setupContentGate(element);
observer.unobserve(element);
}
}
});
});
// Observe all secure content elements
document.querySelectorAll('[data-secure-content]').forEach(element => {
observer.observe(element);
});
}
parseMarkdown(markdown) {
// Simple markdown parser - replace with full parser in production
return markdown
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
.replace(/\*(.*)\*/gim, '<em>$1</em>')
.replace(/\n/gim, '<br>');
}
renderJSONContent(data) {
// Custom JSON content renderer
if (data.type === 'video') {
return `
<div class="video-content">
<video controls>
<source src="${data.url}" type="video/mp4">
</video>
<h3>${data.title}</h3>
<p>${data.description}</p>
</div>
`;
} else if (data.type === 'document') {
return `
<div class="document-content">
<h3>${data.title}</h3>
<div class="document-body">${data.body}</div>
<a href="${data.downloadUrl}" class="download-link">Download PDF</a>
</div>
`;
}
return `<pre>${JSON.stringify(data, null, 2)}</pre>`;
}
}
// Initialize secure content manager
document.addEventListener('DOMContentLoaded', () => {
new SecureContentManager();
});
```
## Comments System
### Posts Management
#### getPosts()
Retrieve posts from a comment channel.
**Method Signature:**
```typescript
await memberstack.getPosts({
channelKey: string;
order?: "newest" | "oldest";
after?: string;
limit?: number;
}): Promise<GetPostsPayload>
```
**Examples:**
Load Comment Posts:
```javascript
async function loadPosts(channelKey, options = {}) {
try {
const result = await memberstack.getPosts({
channelKey,
order: options.order || 'newest',
limit: options.limit || 10,
after: options.after
});
console.log('Posts loaded:', result.data);
return result.data;
} catch (error) {
console.error('Failed to load posts:', error);
throw error;
}
}
class CommentsSystem {
constructor(channelKey) {
this.channelKey = channelKey;
this.memberstack = window.$memberstackDom;
this.posts = [];
this.currentMember = null;
this.init();
}
async init() {
await this.loadCurrentMember();
await this.loadPosts();
this.setupUI();
this.setupEventListeners();
}
async loadCurrentMember() {
try {
const result = await this.memberstack.getCurrentMember();
this.currentMember = result.data;
} catch (error) {
console.error('Failed to load current member:', error);
}
}
async loadPosts() {
try {
const result = await this.memberstack.getPosts({
channelKey: this.channelKey,
order: 'newest',
limit: 20
});
this.posts = result.data.posts || [];
this.renderPosts();
} catch (error) {
console.error('Failed to load posts:', error);
this.showError('Failed to load comments');
}
}
setupUI() {
const container = document.getElementById('comments-container');
container.innerHTML = `
<div class="comments-header">
<h3>Comments (${this.posts.length})</h3>
</div>
<div class="comment-form">
${this.currentMember ? `
<div class="user-avatar">
<img src="${this.currentMember.profileImage || '/default-avatar.png'}" alt="Your avatar">
</div>
<div class="form-content">
<textarea id="new-comment" placeholder="Write a comment..."></textarea>
<button id="submit-comment" class="btn">Post Comment</button>
</div>
` : `
<div class="login-prompt">
<p>Please log in to join the discussion</p>
<button onclick="memberstack.openModal('LOGIN')">Sign In</button>
</div>
`}
</div>
<div id="posts-list" class="posts-list">
<!-- Posts will be rendered here -->
</div>
<div id="load-more" class="load-more" style="display: none;">
<button onclick="this.loadMorePosts()">Load More Comments</button>
</div>
`;
}
renderPosts() {
const postsContainer = document.getElementById('posts-list');
postsContainer.innerHTML = this.posts.map(post => `
<div class="post" data-post-id="${post.id}">
<div class="post-header">
<img src="${post.author.profileImage || '/default-avatar.png'}"
alt="${post.author.name}" class="author-avatar">
<div class="author-info">
<span class="author-name">${post.author.name}</span>
<span class="post-date">${this.formatDate(post.createdAt)}</span>
</div>
${this.canEditPost(post) ? `
<div class="post-actions">
<button onclick="this.editPost('${post.id}')" class="edit-btn">Edit</button>
<button onclick="this.deletePost('${post.id}')" class="delete-btn">Delete</button>
</div>
` : ''}
</div>
<div class="post-content">
${post.content}
</div>
<div class="post-footer">
<div class="post-voting">
<button onclick="this.votePost('${post.id}', 'UP')"
class="vote-btn ${post.userVote === 'UP' ? 'active' : ''}">
👍 ${post.upvotes || 0}
</button>
<button onclick="this.votePost('${post.id}', 'DOWN')"
class="vote-btn ${post.userVote === 'DOWN' ? 'active' : ''}">
👎 ${post.downvotes || 0}
</button>
</div>
<button onclick="this.toggleThreads('${post.id}')" class="replies-btn">
${post.threadCount || 0} replies
</button>
</div>
<div id="threads-${post.id}" class="threads-container" style="display: none;">
<!-- Threads will be loaded here -->
</div>
</div>
`).join('');
}
setupEventListeners() {
// Submit new comment
document.getElementById('submit-comment')?.addEventListener('click', () => {
this.submitComment();
});
// Enter key to submit
document.getElementById('new-comment')?.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && e.ctrlKey) {
this.submitComment();
}
});
}
async submitComment() {
const textarea = document.getElementById('new-comment');
const content = textarea.value.trim();
if (!content) {
alert('Please enter a comment');
return;
}
if (!this.currentMember) {
memberstack.openModal('LOGIN');
return;
}
try {
const result = await this.memberstack.createPost({
channelKey: this.channelKey,
content: content
});
// Add new post to the beginning of the list
this.posts.unshift(result.data);
this.renderPosts();
// Clear the form
textarea.value = '';
console.log('Comment posted:', result.data);
} catch (error) {
console.error('Failed to post comment:', error);
alert('Failed to post comment. Please try again.');
}
}
canEditPost(post) {
return this.currentMember &&
(this.currentMember.id === post.author.id || this.currentMember.isAdmin);
}
formatDate(dateString) {
const date = new Date(dateString);
const now = new Date();
const diffMinutes = Math.floor((now - date) / (1000 * 60));
if (diffMinutes < 1) return 'Just now';
if (diffMinutes < 60) return `${diffMinutes}m ago`;
if (diffMinutes < 1440) return `${Math.floor(diffMinutes / 60)}h ago`;
return date.toLocaleDateString();
}
}
// Initialize comments system
document.addEventListener('DOMContentLoaded', () => {
const channelKey = document.querySelector('[data-comments-channel]')?.dataset.commentsChannel;
if (channelKey) {
new CommentsSystem(channelKey);
}
});
```
#### createPost()
Create a new post in a comment channel.
**Method Signature:**
```typescript
await memberstack.createPost({
channelKey: string;
content: string;
}): Promise<CreatePostPayload>
```
#### updatePost()
Update an existing post.
**Method Signature:**
```typescript
await memberstack.updatePost({
postId: string;
content: string;
}): Promise<UpdatePostPayload>
```
#### deletePost()
Delete a post.
**Method Signature:**
```typescript
await memberstack.deletePost({
postId: string;
}): Promise<void>
```
#### postVote()
Vote on a post (upvote/downvote).
**Method Signature:**
```typescript
await memberstack.postVote({
postId: string;
vote: "UP" | "DOWN" | "NONE";
}): Promise<void>
```
### Threads Management
#### getThreads()
Get replies (threads) for a specific post.
**Method Signature:**
```typescript
await memberstack.getThreads({
postId: string;
order?: "newest" | "oldest";
after?: string;
limit?: number;
}): Promise<GetThreadsPayload>
```
#### createThread()
Create a reply to a post.
**Method Signature:**
```typescript
await memberstack.createThread({
postId: string;
content: string;
}): Promise<CreateThreadPayload>
```
**Complete Comments Implementation Example:**
```javascript
// Extended comments system with threads support
class AdvancedCommentsSystem extends CommentsSystem {
constructor(channelKey) {
super(channelKey);
this.loadedThreads = new Set();
}
async toggleThreads(postId) {
const threadsContainer = document.getElementById(`threads-${postId}`);
if (threadsContainer.style.display === 'none') {
// Load and show threads
await this.loadThreads(postId);
threadsContainer.style.display = 'block';
} else {
// Hide threads
threadsContainer.style.display = 'none';
}
}
async loadThreads(postId) {
if (this.loadedThreads.has(postId)) return;
try {
const result = await this.memberstack.getThreads({
postId: postId,
order: 'oldest',
limit: 20
});
this.renderThreads(postId, result.data.threads || []);
this.loadedThreads.add(postId);
} catch (error) {
console.error('Failed to load threads:', error);
}
}
renderThreads(postId, threads) {
const threadsContainer = document.getElementById(`threads-${postId}`);
threadsContainer.innerHTML = `
<div class="thread-form">
${this.currentMember ? `
<textarea placeholder="Write a reply..." id="reply-${postId}"></textarea>
<button onclick="this.submitThread('${postId}')" class="btn-small">Reply</button>
` : ''}
</div>
<div class="threads-list">
${threads.map(thread => `
<div class="thread" data-thread-id="${thread.id}">
<div class="thread-header">
<img src="${thread.author.profileImage || '/default-avatar.png'}"
alt="${thread.author.name}" class="author-avatar-small">
<span class="author-name">${thread.author.name}</span>
<span class="thread-date">${this.formatDate(thread.createdAt)}</span>
</div>
<div class="thread-content">${thread.content}</div>
<div class="thread-voting">
<button onclick="this.voteThread('${thread.id}', 'UP')"
class="vote-btn-small ${thread.userVote === 'UP' ? 'active' : ''}">
👍 ${thread.upvotes || 0}
</button>
<button onclick="this.voteThread('${thread.id}', 'DOWN')"
class="vote-btn-small ${thread.userVote === 'DOWN' ? 'active' : ''}">
👎 ${thread.downvotes || 0}
</button>
</div>
</div>
`).join('')}
</div>
`;
}
async submitThread(postId) {
const textarea = document.getElementById(`reply-${postId}`);
const content = textarea.value.trim();
if (!content) return;
try {
const result = await this.memberstack.createThread({
postId: postId,
content: content
});
// Reload threads to show the new reply
this.loadedThreads.delete(postId);
await this.loadThreads(postId);
textarea.value = '';
console.log('Thread created:', result.data);
} catch (error) {
console.error('Failed to create thread:', error);
alert('Failed to post reply. Please try again.');
}
}
async votePost(postId, vote) {
if (!this.currentMember) {
memberstack.openModal('LOGIN');
return;
}
try {
await this.memberstack.postVote({ postId, vote });
// Reload posts to update vote counts
await this.loadPosts();
} catch (error) {
console.error('Failed to vote on post:', error);
}
}
async voteThread(threadId, vote) {
if (!this.currentMember) {
memberstack.openModal('LOGIN');
return;
}
try {
await this.memberstack.threadVote({ threadId, vote });
// Find the post this thread belongs to and reload threads
const post = this.posts.find(p =>
document.querySelector(`[data-thread-id="${threadId}"]`)
?.closest(`[data-post-id]`)
?.dataset.postId === p.id
);
if (post) {
this.loadedThreads.delete(post.id);
await this.loadThreads(post.id);
}
} catch (error) {
console.error('Failed to vote on thread:', error);
}
}
}
```
## Team Management
### joinTeam()
Join a team using an invitation token.
**Method Signature:**
```typescript
await memberstack.joinTeam({
inviteToken: string;
}): Promise<void>
```
### getTeam()
Get information about a team.
**Method Signature:**
```typescript
await memberstack.getTeam({
teamId: string;
}): Promise<GetTeamPayload>
```
### generateInviteToken()
Generate an invitation token for a team.
**Method Signature:**
```typescript
await memberstack.generateInviteToken({
teamId: string;
}): Promise<GenerateInviteTokenPayload>
```
### removeMemberFromTeam()
Remove a member from a team.
**Method Signature:**
```typescript
await memberstack.removeMemberFromTeam({
teamId: string;
memberId: string;
}): Promise<void>
```
**Complete Team Management Example:**
```javascript
class TeamManager {
constructor() {
this.memberstack = window.$memberstackDom;
this.currentTeam = null;
this.init();
}
async init() {
await this.loadCurrentTeam();
this.setupUI();
this.handleInviteToken();
}
async loadCurrentTeam() {
try {
const member = await this.memberstack.getCurrentMember();
if (member.data && member.data.teamId) {
const team = await this.memberstack.getTeam({
teamId: member.data.teamId
});
this.currentTeam = team.data;
}
} catch (error) {
console.error('Failed to load team:', error);
}
}
setupUI() {
const container = document.getElementById('team-container');
if (this.currentTeam) {
this.renderTeamDashboard(container);
} else {
this.renderJoinTeamPrompt(container);
}
}
renderTeamDashboard(container) {
container.innerHTML = `
<div class="team-dashboard">
<h2>${this.currentTeam.name}</h2>
<p>Members: ${this.currentTeam.memberCount}</p>
<div class="team-actions">
<button onclick="this.generateInviteLink()">Generate Invite Link</button>
<button onclick="this.showTeamMembers()">View Members</button>
</div>
<div id="invite-link-section" style="display: none;">
<h3>Team Invite Link</h3>
<div class="invite-link-container">
<input type="text" id="invite-link" readonly>
<button onclick="this.copyInviteLink()">Copy Link</button>
</div>
</div>
<div id="team-members" style="display: none;">
<!-- Team members will be loaded here -->
</div>
</div>
`;
}
renderJoinTeamPrompt(container) {
container.innerHTML = `
<div class="join-team">
<h2>Join a Team</h2>
<p>Enter an invitation code to join a team:</p>
<div class="join-form">
<input type="text" id="invite-token" placeholder="Enter invitation code">
<button onclick="this.joinTeamWithToken()">Join Team</button>
</div>
</div>
`;
}
async generateInviteLink() {
try {
const result = await this.memberstack.generateInviteToken({
teamId: this.currentTeam.id
});
const inviteUrl = `${window.location.origin}/join-team?token=${result.data.token}`;
document.getElementById('invite-link').value = inviteUrl;
document.getElementById('invite-link-section').style.display = 'block';
console.log('Invite link generated:', inviteUrl);
} catch (error) {
console.error('Failed to generate invite link:', error);
alert('Failed to generate invite link');
}
}
copyInviteLink() {
const linkInput = document.getElementById('invite-link');
linkInput.select();
document.execCommand('copy');
// Show feedback
const button = event.target;
const originalText = button.textContent;
button.textContent = 'Copied!';
setTimeout(() => {
button.textContent = originalText;
}, 2000);
}
async joinTeamWithToken() {
const token = document.getElementById('invite-token').value.trim();
if (!token) {
alert('Please enter an invitation code');
return;
}
try {
await this.memberstack.joinTeam({
inviteToken: token
});
alert('Successfully joined the team!');
// Reload the page to show team dashboard
window.location.reload();
} catch (error) {
console.error('Failed to join team:', error);
const errorMessages = {
'INVALID_TOKEN': 'Invalid invitation code',
'EXPIRED_TOKEN': 'This invitation has expired',
'ALREADY_MEMBER': 'You are already a member of this team'
};
alert(errorMessages[error.code] || 'Failed to join team');
}
}
handleInviteToken() {
// Check if there's an invite token in the URL
const params = new URLSearchParams(window.location.search);
const inviteToken = params.get('token');
if (inviteToken) {
const confirmed = confirm('You\'ve been invited to join a team. Would you like to join?');
if (confirmed) {
document.getElementById('invite-token').value = inviteToken;
this.joinTeamWithToken();
}
}
}
async showTeamMembers() {
const membersContainer = document.getElementById('team-members');
// This would typically load team members from your backend
// Since the DOM package doesn't have a direct method for this,
// you'd implement this with your own API
membersContainer.innerHTML = `
<h3>Team Members</h3>
<div class="members-list">
<!-- Team members would be listed here -->
<p>Member management features require custom implementation</p>
</div>
`;
membersContainer.style.display = 'block';
}
}
// Initialize team manager
document.addEventListener('DOMContentLoaded', () => {
new TeamManager();
});
```
## Event Tracking
### _Event()
Track custom events for analytics (internal method).
**Method Signature:**
```typescript
await memberstack._Event({
data: {
eventName: string;
properties: Record<string, any>;
};
}): Promise<void>
```
**Example:**
```javascript
async function trackCustomEvent(eventName, properties = {}) {
try {
await memberstack._Event({
data: {
eventName: eventName,
properties: {
...properties,
timestamp: new Date().toISOString(),
url: window.location.href,
userAgent: navigator.userAgent
}
}
});
console.log('Event tracked:', eventName, properties);
} catch (error) {
console.error('Failed to track event:', error);
}
}
// Usage examples
trackCustomEvent('premium_content_viewed', {
contentId: 'tutorial-123',
contentType: 'video',
duration: 300
});
trackCustomEvent('team_invite_sent', {
teamId: 'team-456',
inviteMethod: 'link'
});
```
## Next Steps
- **[04-plan-management.md](04-plan-management.md)** - Plan-based access control
- **[08-types-reference.md](08-types-reference.md)** - TypeScript definitions for advanced features
- **[09-error-handling.md](09-error-handling.md)** - Handling advanced feature errors
- **[10-examples.md](10-examples.md)** - Complete implementation examples